Compare commits

...

38 Commits

Author SHA1 Message Date
Andras Bacsai
c735ff545e Merge pull request #1226 from coollabsio/next
v4.0.0-beta.37
2023-09-15 12:44:50 +02:00
Andras Bacsai
b4f048b028 fixes 2023-09-15 12:43:03 +02:00
Andras Bacsai
54a57d217f fix: ssh-agent revert 2023-09-15 12:30:25 +02:00
Andras Bacsai
cf28490acc feat: generate ssh key 2023-09-15 11:55:58 +02:00
Andras Bacsai
019670d5d1 fix: smtp view 2023-09-15 11:28:44 +02:00
Andras Bacsai
b07cc500e7 fix: invitation 2023-09-15 11:19:36 +02:00
Andras Bacsai
82c235d5af add features to pricing 2023-09-14 23:32:58 +02:00
Andras Bacsai
307e4a6990 fix: collect billing address 2023-09-14 20:42:12 +02:00
Andras Bacsai
ddb7af63a6 fix 2023-09-14 20:39:33 +02:00
Andras Bacsai
7a429ee5bb fix: rate limit 2023-09-14 18:49:15 +02:00
Andras Bacsai
bb591604ac workflow update 2023-09-14 18:44:55 +02:00
Andras Bacsai
81f7a65dd5 fix: help 2023-09-14 18:41:21 +02:00
Andras Bacsai
4b313bb1c6 fix: simply reply to help messages 2023-09-14 18:33:05 +02:00
Andras Bacsai
aba0b2f13c remove unnecessary jobs 2023-09-14 18:29:54 +02:00
Andras Bacsai
9a284e47da version++ 2023-09-14 18:22:25 +02:00
Andras Bacsai
9f2fbc661a fix: registration
fix: user deletion
2023-09-14 18:22:08 +02:00
Andras Bacsai
949407368e fix: uniqueips 2023-09-14 18:10:13 +02:00
Andras Bacsai
93c65f6a79 fix: ip check 2023-09-14 18:07:29 +02:00
Andras Bacsai
d9fe16a3ee fix: redirect on server not found 2023-09-14 17:45:00 +02:00
Andras Bacsai
adaca4d4e3 fix: sub for root 2023-09-14 17:32:33 +02:00
Andras Bacsai
a6d5f3038c fix: help uri 2023-09-14 17:28:58 +02:00
Andras Bacsai
4d49132821 hm 2023-09-14 17:22:21 +02:00
Andras Bacsai
c287276d0e temporary fix for proxy 2023-09-14 17:10:37 +02:00
Andras Bacsai
e89868c692 fix: SaveConfigurationSync 2023-09-14 16:55:13 +02:00
Andras Bacsai
f93317cd2e Merge pull request #1224 from coollabsio/next
v4.0.0-beta.36
2023-09-14 16:28:52 +02:00
Andras Bacsai
8412802f4d oh wow, it is cool! 2023-09-14 15:52:04 +02:00
Andras Bacsai
53c20e1e99 feat: new container status checks 2023-09-14 12:45:50 +02:00
Andras Bacsai
3c8c8e20b1 fix: editable ip
fix: traefik dashboard
2023-09-14 11:37:20 +02:00
Andras Bacsai
49f8abcd79 remove unnecessary things 2023-09-14 10:56:25 +02:00
Andras Bacsai
4a4d73b87b fix: plus boarding step about Coolify 2023-09-14 10:39:05 +02:00
Andras Bacsai
046eab3776 fix: processWithEnv()->run 2023-09-14 10:26:48 +02:00
Andras Bacsai
17c0e91a0d feat: ssh-agent instead of filesystem based ssh keys 2023-09-14 10:12:58 +02:00
Andras Bacsai
fe4a0ae166 fix: encrypt jobs 2023-09-14 10:12:44 +02:00
Andras Bacsai
52c84f8d22 fix: lower case email on waitlist 2023-09-13 20:48:13 +02:00
Andras Bacsai
b22fecb615 fix: lowercase email in forgot password 2023-09-13 20:27:58 +02:00
Andras Bacsai
23b7fc3c54 fix: prevent weird ui bug for validateServer 2023-09-13 13:00:43 +02:00
Andras Bacsai
1efb1235b4 fix: add timeout for ssh commands 2023-09-13 13:00:16 +02:00
Andras Bacsai
0924070a13 version++ 2023-09-13 12:42:59 +02:00
102 changed files with 1187 additions and 713 deletions

View File

@@ -52,7 +52,7 @@ jobs:
push: true push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64
merge-manifest: merge-manifest:
runs-on: [self-hosted, x64] runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
packages: write packages: write

View File

@@ -10,7 +10,7 @@ env:
jobs: jobs:
amd64: amd64:
runs-on: ubuntu-latest runs-on: [self-hosted, x64]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Login to ghcr.io - name: Login to ghcr.io

View File

@@ -10,9 +10,6 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
const TIMEOUT = 3600;
const IDLE_TIMEOUT = 3600;
class RunRemoteProcess class RunRemoteProcess
{ {
public Activity $activity; public Activity $activity;
@@ -76,8 +73,7 @@ class RunRemoteProcess
$this->time_start = hrtime(true); $this->time_start = hrtime(true);
$status = ProcessStatus::IN_PROGRESS; $status = ProcessStatus::IN_PROGRESS;
$processResult = Process::forever()->run($this->getCommand(), $this->handleOutput(...));
$processResult = Process::timeout(TIMEOUT)->idleTimeout(IDLE_TIMEOUT)->run($this->getCommand(), $this->handleOutput(...));
if ($this->activity->properties->get('status') === ProcessStatus::ERROR->value) { if ($this->activity->properties->get('status') === ProcessStatus::ERROR->value) {
$status = ProcessStatus::ERROR; $status = ProcessStatus::ERROR;
@@ -108,11 +104,10 @@ class RunRemoteProcess
{ {
$user = $this->activity->getExtraProperty('user'); $user = $this->activity->getExtraProperty('user');
$server_ip = $this->activity->getExtraProperty('server_ip'); $server_ip = $this->activity->getExtraProperty('server_ip');
$private_key_location = $this->activity->getExtraProperty('private_key_location');
$port = $this->activity->getExtraProperty('port'); $port = $this->activity->getExtraProperty('port');
$command = $this->activity->getExtraProperty('command'); $command = $this->activity->getExtraProperty('command');
return generate_ssh_command($private_key_location, $server_ip, $user, $port, $command); return generateSshCommand($server_ip, $user, $port, $command);
} }
protected function handleOutput(string $type, string $output) protected function handleOutput(string $type, string $output)

View File

@@ -16,10 +16,7 @@ class CheckConfigurationSync
if ($reset || is_null($proxy_configuration)) { if ($reset || is_null($proxy_configuration)) {
$proxy_configuration = Str::of(generate_default_proxy_configuration($server))->trim()->value; $proxy_configuration = Str::of(generate_default_proxy_configuration($server))->trim()->value;
resolve(SaveConfigurationSync::class)($server, $proxy_configuration);
return $proxy_configuration;
} }
return $proxy_configuration; return $proxy_configuration;
} }
} }

View File

@@ -7,11 +7,12 @@ use Illuminate\Support\Str;
class SaveConfigurationSync class SaveConfigurationSync
{ {
public function __invoke(Server $server, string $configuration) public function __invoke(Server $server)
{ {
try { try {
$proxy_settings = resolve(CheckConfigurationSync::class)($server, true);
$proxy_path = get_proxy_path(); $proxy_path = get_proxy_path();
$docker_compose_yml_base64 = base64_encode($configuration); $docker_compose_yml_base64 = base64_encode($proxy_settings);
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; $server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save(); $server->save();

View File

@@ -8,7 +8,7 @@ use Spatie\Activitylog\Models\Activity;
class StartProxy class StartProxy
{ {
public function __invoke(Server $server): Activity public function __invoke(Server $server, bool $async = true): Activity|string
{ {
$proxy_path = get_proxy_path(); $proxy_path = get_proxy_path();
$networks = collect($server->standaloneDockers)->map(function ($docker) { $networks = collect($server->standaloneDockers)->map(function ($docker) {
@@ -26,8 +26,7 @@ class StartProxy
$docker_compose_yml_base64 = base64_encode($configuration); $docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; $server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save(); $server->save();
$commands = [
$activity = remote_process([
"echo '####### Creating required Docker networks...'", "echo '####### Creating required Docker networks...'",
...$create_networks_command, ...$create_networks_command,
"cd $proxy_path", "cd $proxy_path",
@@ -44,8 +43,13 @@ class StartProxy
"echo '####### Starting coolify-proxy...'", "echo '####### Starting coolify-proxy...'",
'docker compose up -d --remove-orphans', 'docker compose up -d --remove-orphans',
"echo '####### Proxy installed successfully...'" "echo '####### Proxy installed successfully...'"
], $server); ];
if (!$async) {
return $activity; instant_remote_process($commands, $server);
return 'OK';
} else {
$activity = remote_process($commands, $server);
return $activity;
}
} }
} }

View File

@@ -24,7 +24,7 @@ use Illuminate\Console\Command;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Mail; use Mail;
use Str; use Illuminate\Support\Str;
use function Laravel\Prompts\confirm; use function Laravel\Prompts\confirm;
use function Laravel\Prompts\select; use function Laravel\Prompts\select;
@@ -62,7 +62,7 @@ class Emails extends Command
'application-status-changed' => 'Application - Status Changed', 'application-status-changed' => 'Application - Status Changed',
'backup-success' => 'Database - Backup Success', 'backup-success' => 'Database - Backup Success',
'backup-failed' => 'Database - Backup Failed', 'backup-failed' => 'Database - Backup Failed',
'invitation-link' => 'Invitation Link', // 'invitation-link' => 'Invitation Link',
'waitlist-invitation-link' => 'Waitlist Invitation Link', 'waitlist-invitation-link' => 'Waitlist Invitation Link',
'waitlist-confirmation' => 'Waitlist Confirmation', 'waitlist-confirmation' => 'Waitlist Confirmation',
'realusers-before-trial' => 'REAL - Registered Users Before Trial without Subscription', 'realusers-before-trial' => 'REAL - Registered Users Before Trial without Subscription',
@@ -141,20 +141,20 @@ class Emails extends Command
$this->mail = (new BackupSuccess($backup, $db))->toMail(); $this->mail = (new BackupSuccess($backup, $db))->toMail();
$this->sendEmail(); $this->sendEmail();
break; break;
case 'invitation-link': // case 'invitation-link':
$user = User::all()->first(); // $user = User::all()->first();
$invitation = TeamInvitation::whereEmail($user->email)->first(); // $invitation = TeamInvitation::whereEmail($user->email)->first();
if (!$invitation) { // if (!$invitation) {
$invitation = TeamInvitation::create([ // $invitation = TeamInvitation::create([
'uuid' => Str::uuid(), // 'uuid' => Str::uuid(),
'email' => $user->email, // 'email' => $user->email,
'team_id' => 1, // 'team_id' => 1,
'link' => 'http://example.com', // 'link' => 'http://example.com',
]); // ]);
} // }
$this->mail = (new InvitationLink($user))->toMail(); // $this->mail = (new InvitationLink($user))->toMail();
$this->sendEmail(); // $this->sendEmail();
break; // break;
case 'waitlist-invitation-link': case 'waitlist-invitation-link':
$this->mail = new MailMessage(); $this->mail = new MailMessage();
$this->mail->view('emails.waitlist-invitation', [ $this->mail->view('emails.waitlist-invitation', [

View File

@@ -2,21 +2,15 @@
namespace App\Console; namespace App\Console;
use App\Enums\ProxyTypes;
use App\Jobs\ApplicationContainerStatusJob;
use App\Jobs\CheckResaleLicenseJob; use App\Jobs\CheckResaleLicenseJob;
use App\Jobs\CleanupInstanceStuffsJob; use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\DatabaseBackupJob; use App\Jobs\DatabaseBackupJob;
use App\Jobs\DatabaseContainerStatusJob;
use App\Jobs\DockerCleanupJob; use App\Jobs\DockerCleanupJob;
use App\Jobs\InstanceAutoUpdateJob; use App\Jobs\InstanceAutoUpdateJob;
use App\Jobs\ProxyContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Jobs\ServerDetailsCheckJob;
use App\Models\Application;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\Server; use App\Models\Server;
use App\Models\StandalonePostgresql;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -25,43 +19,31 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void protected function schedule(Schedule $schedule): void
{ {
if (isDev()) { if (isDev()) {
$schedule->job(new ServerDetailsCheckJob(Server::find(0)))->everyTenMinutes()->onOneServer(); // $schedule->job(new ContainerStatusJob(Server::find(0)))->everyTenMinutes()->onOneServer();
// $schedule->command('horizon:snapshot')->everyMinute(); // $schedule->command('horizon:snapshot')->everyMinute();
// $schedule->job(new CleanupInstanceStuffsJob)->everyMinute(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
// $schedule->job(new CheckResaleLicenseJob)->hourly(); // $schedule->job(new CheckResaleLicenseJob)->hourly();
// $schedule->job(new DockerCleanupJob)->everyOddHour(); // $schedule->job(new DockerCleanupJob)->everyOddHour();
// $this->instance_auto_update($schedule); // $this->instance_auto_update($schedule);
// $this->check_scheduled_backups($schedule); // $this->check_scheduled_backups($schedule);
// $this->check_resources($schedule); $this->check_resources($schedule);
// $this->check_proxies($schedule);
} else { } else {
$schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyTenMinutes()->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
$schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer(); $schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer();
$this->instance_auto_update($schedule); $this->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule); $this->check_scheduled_backups($schedule);
$this->check_resources($schedule); $this->check_resources($schedule);
$this->check_proxies($schedule);
}
}
private function check_proxies($schedule)
{
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true)->whereNotNull('proxy.type')->where('proxy.type', '!=', ProxyTypes::NONE->value);
foreach ($servers as $server) {
$schedule->job(new ProxyContainerStatusJob($server))->everyMinute()->onOneServer();
} }
} }
private function check_resources($schedule) private function check_resources($schedule)
{ {
$applications = Application::all(); $servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true);
foreach ($applications as $application) { ray($servers);
$schedule->job(new ApplicationContainerStatusJob($application))->everyMinute()->onOneServer();
}
$postgresqls = StandalonePostgresql::all(); foreach ($servers as $server) {
foreach ($postgresqls as $postgresql) { $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
$schedule->job(new DatabaseContainerStatusJob($postgresql))->everyMinute()->onOneServer();
} }
} }
private function instance_auto_update($schedule) private function instance_auto_update($schedule)

View File

@@ -13,7 +13,6 @@ class CoolifyTaskArgs extends Data
{ {
public function __construct( public function __construct(
public string $server_ip, public string $server_ip,
public string $private_key_location,
public string $command, public string $command,
public int $port, public int $port,
public string $user, public string $user,

View File

@@ -3,21 +3,18 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\S3Storage; use App\Models\S3Storage;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\TeamInvitation; use App\Models\TeamInvitation;
use App\Models\User; use App\Models\User;
use Auth;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController; use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Str;
use Throwable; use Throwable;
use Str;
class Controller extends BaseController class Controller extends BaseController
{ {
@@ -35,8 +32,15 @@ class Controller extends BaseController
return redirect()->route('login'); return redirect()->route('login');
} }
if (Hash::check($password, $user->password)) { if (Hash::check($password, $user->password)) {
$invitation = TeamInvitation::whereEmail($email);
if ($invitation->exists()) {
$team = $invitation->first()->team;
$user->teams()->attach($team->id, ['role' => $invitation->first()->role]);
$invitation->delete();
} else {
$team = $user->teams()->first();
}
Auth::login($user); Auth::login($user);
$team = $user->teams()->first();
session(['currentTeam' => $team]); session(['currentTeam' => $team]);
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
@@ -137,24 +141,20 @@ class Controller extends BaseController
try { try {
$invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail(); $invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail();
$user = User::whereEmail($invitation->email)->firstOrFail(); $user = User::whereEmail($invitation->email)->firstOrFail();
if (is_null(auth()->user())) {
return redirect()->route('login');
}
if (auth()->user()->id !== $user->id) { if (auth()->user()->id !== $user->id) {
abort(401); abort(401);
} }
$invitationValid = $invitation->isValid();
$createdAt = $invitation->created_at; if ($invitationValid) {
$diff = $createdAt->diffInMinutes(now());
if ($diff <= config('constants.invitation.link.expiration')) {
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
refreshSession($invitation->team);
$invitation->delete(); $invitation->delete();
return redirect()->route('team.index'); return redirect()->route('team.index');
} else { } else {
$invitation->delete();
abort(401); abort(401);
} }
} catch (Throwable $e) { } catch (Throwable $e) {
ray($e->getMessage());
throw $e; throw $e;
} }
} }

View File

@@ -53,12 +53,13 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->remoteServerHost = 'coolify-testing-host'; $this->remoteServerHost = 'coolify-testing-host';
} }
} }
public function welcome() { public function explanation() {
if (isCloud()) { if (isCloud()) {
return $this->setServerType('remote'); return $this->setServerType('remote');
} }
$this->currentState = 'select-server-type'; $this->currentState = 'select-server-type';
} }
public function restartBoarding() public function restartBoarding()
{ {
if ($this->createdServer) { if ($this->createdServer) {

View File

@@ -19,7 +19,7 @@ class Help extends Component
]; ];
public function mount() public function mount()
{ {
$this->path = Route::current()->uri(); $this->path = Route::current()?->uri() ?? null;
if (isDev()) { if (isDev()) {
$this->description = "I'm having trouble with {$this->path}"; $this->description = "I'm having trouble with {$this->path}";
$this->subject = "Help with {$this->path}"; $this->subject = "Help with {$this->path}";
@@ -41,7 +41,7 @@ class Help extends Component
] ]
); );
$mail->subject("[HELP - {$subscriptionType}]: {$this->subject}"); $mail->subject("[HELP - {$subscriptionType}]: {$this->subject}");
send_user_an_email($mail, 'hi@coollabs.io', auth()->user()?->email); send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io');
$this->emit('success', 'Your message has been sent successfully. We will get in touch with you as soon as possible.'); $this->emit('success', 'Your message has been sent successfully. We will get in touch with you as soon as possible.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return general_error_handler($e, $this); return general_error_handler($e, $this);

View File

@@ -46,9 +46,6 @@ class DiscordSettings extends Component
public function saveModel() public function saveModel()
{ {
$this->team->save(); $this->team->save();
if (is_a($this->team, Team::class)) {
refreshSession();
}
$this->emit('success', 'Settings saved.'); $this->emit('success', 'Settings saved.');
} }

View File

@@ -110,9 +110,6 @@ class EmailSettings extends Component
public function saveModel() public function saveModel()
{ {
$this->team->save(); $this->team->save();
if (is_a($this->team, Team::class)) {
refreshSession();
}
$this->emit('success', 'Settings saved.'); $this->emit('success', 'Settings saved.');
} }
public function submit() public function submit()
@@ -141,10 +138,11 @@ class EmailSettings extends Component
try { try {
$this->resetErrorBag(); $this->resetErrorBag();
$this->validate([ $this->validate([
'team.smtp_from_address' => 'required|email',
'team.smtp_from_name' => 'required',
'team.resend_api_key' => 'required' 'team.resend_api_key' => 'required'
]); ]);
$this->team->save(); $this->team->save();
refreshSession();
$this->emit('success', 'Settings saved successfully.'); $this->emit('success', 'Settings saved successfully.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->team->resend_enabled = false; $this->team->resend_enabled = false;

View File

@@ -52,9 +52,6 @@ class TelegramSettings extends Component
public function saveModel() public function saveModel()
{ {
$this->team->save(); $this->team->save();
if (is_a($this->team, Team::class)) {
refreshSession();
}
$this->emit('success', 'Settings saved.'); $this->emit('success', 'Settings saved.');
} }

View File

@@ -4,13 +4,15 @@ namespace App\Http\Livewire\PrivateKey;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use Livewire\Component; use Livewire\Component;
use phpseclib3\Crypt\PublicKeyLoader;
class Create extends Component class Create extends Component
{ {
public string|null $from = null; public ?string $from = null;
public string $name; public string $name;
public string|null $description = null; public ?string $description = null;
public string $value; public string $value;
public ?string $publicKey = null;
protected $rules = [ protected $rules = [
'name' => 'required|string', 'name' => 'required|string',
'value' => 'required|string', 'value' => 'required|string',
@@ -20,6 +22,23 @@ class Create extends Component
'value' => 'private Key', 'value' => 'private Key',
]; ];
public function generateNewKey()
{
$this->name = generate_random_name();
$this->description = 'Created by Coolify';
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey();
}
public function updated($updateProperty)
{
if ($updateProperty === 'value') {
try {
$this->publicKey = PublicKeyLoader::load($this->$updateProperty)->getPublicKey()->toString('OpenSSH',['comment' => '']);
} catch (\Throwable $e) {
$this->publicKey = "Invalid private key";
}
}
$this->validateOnly($updateProperty);
}
public function createPrivateKey() public function createPrivateKey()
{ {
$this->validate(); $this->validate();

View File

@@ -2,9 +2,8 @@
namespace App\Http\Livewire\Project\Application; namespace App\Http\Livewire\Project\Application;
use App\Jobs\ApplicationContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Models\Application; use App\Models\Application;
use App\Notifications\Application\StatusChanged;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -22,10 +21,11 @@ class Heading extends Component
public function check_status() public function check_status()
{ {
dispatch_sync(new ApplicationContainerStatusJob( dispatch_sync(new ContainerStatusJob($this->application->destination->server));
application: $this->application,
));
$this->application->refresh(); $this->application->refresh();
$this->application->previews->each(function ($preview) {
$preview->refresh();
});
} }
public function force_deploy_without_cache() public function force_deploy_without_cache()

View File

@@ -2,7 +2,6 @@
namespace App\Http\Livewire\Project\Application; namespace App\Http\Livewire\Project\Application;
use App\Jobs\ApplicationContainerStatusJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -23,14 +22,6 @@ class Previews extends Component
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
} }
public function loadStatus($pull_request_id)
{
dispatch(new ApplicationContainerStatusJob(
application: $this->application,
pullRequestId: $pull_request_id
));
}
public function load_prs() public function load_prs()
{ {
try { try {

View File

@@ -3,8 +3,7 @@
namespace App\Http\Livewire\Project\Database; namespace App\Http\Livewire\Project\Database;
use App\Actions\Database\StartPostgresql; use App\Actions\Database\StartPostgresql;
use App\Jobs\DatabaseContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Notifications\Application\StatusChanged;
use Livewire\Component; use Livewire\Component;
class Heading extends Component class Heading extends Component
@@ -25,9 +24,7 @@ class Heading extends Component
public function check_status() public function check_status()
{ {
dispatch_sync(new DatabaseContainerStatusJob( dispatch_sync(new ContainerStatusJob($this->database->destination->server));
database: $this->database,
));
$this->database->refresh(); $this->database->refresh();
} }

View File

@@ -5,7 +5,7 @@ namespace App\Http\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable; use App\Models\EnvironmentVariable;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
use Str; use Illuminate\Support\Str;
class All extends Component class All extends Component
{ {

View File

@@ -56,7 +56,7 @@ class Form extends Component
$this->uptime = $uptime; $this->uptime = $uptime;
$this->emit('success', 'Server is reachable!'); $this->emit('success', 'Server is reachable!');
} else { } else {
$this->emit('error', 'Server is not rachable'); $this->emit('error', 'Server is not reachable');
return; return;
} }
if ($dockerVersion) { if ($dockerVersion) {
@@ -88,17 +88,13 @@ class Form extends Component
public function submit() public function submit()
{ {
$this->validate(); $this->validate();
// $validation = Validator::make($this->server->toArray(), [ $uniqueIPs = Server::all()->reject(function (Server $server) {
// 'ip' => [ return $server->id === $this->server->id;
// 'ip' })->pluck('ip')->toArray();
// ], if (in_array($this->server->ip, $uniqueIPs)) {
// ]); $this->emit('error', 'IP address is already in use by another team.');
// if ($validation->fails()) { return;
// foreach ($validation->errors()->getMessages() as $key => $value) { }
// $this->addError("server.{$key}", $value[0]);
// }
// return;
// }
$this->server->settings->wildcard_domain = $this->wildcard_domain; $this->server->settings->wildcard_domain = $this->wildcard_domain;
$this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage; $this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage;
$this->server->settings->save(); $this->server->settings->save();

View File

@@ -48,7 +48,7 @@ class Proxy extends Component
public function submit() public function submit()
{ {
try { try {
resolve(SaveConfigurationSync::class)($this->server, $this->proxy_settings); resolve(SaveConfigurationSync::class)($this->server);
$this->server->proxy->redirect_url = $this->redirect_url; $this->server->proxy->redirect_url = $this->redirect_url;
$this->server->save(); $this->server->save();

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Server\Proxy; namespace App\Http\Livewire\Server\Proxy;
use App\Actions\Proxy\SaveConfigurationSync;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -21,7 +22,7 @@ class Deploy extends Component
$this->server->proxy->last_applied_settings && $this->server->proxy->last_applied_settings &&
$this->server->proxy->last_saved_settings !== $this->server->proxy->last_applied_settings $this->server->proxy->last_saved_settings !== $this->server->proxy->last_applied_settings
) { ) {
resolve(SaveConfigurationSync::class)($this->server, $this->proxy_settings); resolve(SaveConfigurationSync::class)($this->server);
} }
$activity = resolve(StartProxy::class)($this->server); $activity = resolve(StartProxy::class)($this->server);

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Http\Livewire\Server\Proxy;
use App\Models\Server;
use Livewire\Component;
class Modal extends Component
{
public Server $server;
public function proxyStatusUpdated()
{
$this->emit('proxyStatusUpdated');
}
}

View File

@@ -13,7 +13,10 @@ class Show extends Component
public function mount() public function mount()
{ {
try { try {
$this->server = Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail(); $this->server = Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.all');
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this); return general_error_handler(err: $e, that: $this);
} }

View File

@@ -14,6 +14,7 @@ class ShowPrivateKey extends Component
public function setPrivateKey($newPrivateKeyId) public function setPrivateKey($newPrivateKeyId)
{ {
try { try {
refresh_server_connection($this->server->privateKey);
$oldPrivateKeyId = $this->server->private_key_id; $oldPrivateKeyId = $this->server->private_key_id;
$this->server->update([ $this->server->update([
'private_key_id' => $newPrivateKeyId 'private_key_id' => $newPrivateKeyId
@@ -26,7 +27,7 @@ class ShowPrivateKey extends Component
'private_key_id' => $oldPrivateKeyId 'private_key_id' => $oldPrivateKeyId
]); ]);
$this->server->refresh(); $this->server->refresh();
refresh_server_connection($this->server->privateKey); refresh_server_connection($this->server->privateKey);
return general_error_handler($e, that: $this); return general_error_handler($e, that: $this);
} }
} }

View File

@@ -2,7 +2,7 @@
namespace App\Http\Livewire\Settings; namespace App\Http\Livewire\Settings;
use App\Jobs\ProxyContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Models\InstanceSettings as ModelsInstanceSettings; use App\Models\InstanceSettings as ModelsInstanceSettings;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -124,7 +124,7 @@ class Configuration extends Component
]; ];
} }
$this->save_configuration_to_disk($traefik_dynamic_conf, $file); $this->save_configuration_to_disk($traefik_dynamic_conf, $file);
dispatch(new ProxyContainerStatusJob($this->server)); dispatch(new ContainerStatusJob($this->server));
} }
} }

View File

@@ -44,6 +44,7 @@ class PricingPlans extends Component
return; return;
} }
$payload = [ $payload = [
'billing_address_collection' => 'required',
'client_reference_id' => auth()->user()->id . ':' . currentTeam()->id, 'client_reference_id' => auth()->user()->id . ':' . currentTeam()->id,
'line_items' => [[ 'line_items' => [[
'price' => $priceId, 'price' => $priceId,

View File

@@ -12,7 +12,6 @@ class Delete extends Component
$currentTeam = currentTeam(); $currentTeam = currentTeam();
$currentTeam->delete(); $currentTeam->delete();
$team = auth()->user()->teams()->first();
$currentTeam->members->each(function ($user) use ($currentTeam) { $currentTeam->members->each(function ($user) use ($currentTeam) {
if ($user->id === auth()->user()->id) { if ($user->id === auth()->user()->id) {
return; return;

View File

@@ -27,7 +27,6 @@ class Form extends Component
$this->validate(); $this->validate();
try { try {
$this->team->save(); $this->team->save();
refreshSession();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return general_error_handler($e, $this); return general_error_handler($e, $this);
} }

View File

@@ -4,9 +4,13 @@ namespace App\Http\Livewire\Team;
use App\Models\TeamInvitation; use App\Models\TeamInvitation;
use App\Models\User; use App\Models\User;
use App\Notifications\TransactionalEmails\InvitationLink; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Artisan;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class InviteLink extends Component class InviteLink extends Component
{ {
@@ -20,53 +24,68 @@ class InviteLink extends Component
public function viaEmail() public function viaEmail()
{ {
$this->generate_invite_link(isEmail: true); $this->generate_invite_link(sendEmail: true);
} }
private function generate_invite_link(bool $isEmail = false) public function viaLink()
{
$this->generate_invite_link(sendEmail: false);
}
private function generate_invite_link(bool $sendEmail = false)
{ {
try { try {
$uuid = new Cuid2(32);
$link = url('/') . config('constants.invitation.link.base_url') . $uuid;
$user = User::whereEmail($this->email);
if (!$user->exists()) {
return general_error_handler(that: $this, customErrorMessage: "$this->email must be registered first (or activate transactional emails to invite via email).");
}
$member_emails = currentTeam()->members()->get()->pluck('email'); $member_emails = currentTeam()->members()->get()->pluck('email');
if ($member_emails->contains($this->email)) { if ($member_emails->contains($this->email)) {
return general_error_handler(that: $this, customErrorMessage: "$this->email is already a member of " . currentTeam()->name . "."); return general_error_handler(that: $this, customErrorMessage: "$this->email is already a member of " . currentTeam()->name . ".");
} }
$uuid = new Cuid2(32);
$link = url('/') . config('constants.invitation.link.base_url') . $uuid;
$user = User::whereEmail($this->email)->first();
$invitation = TeamInvitation::whereEmail($this->email); if (is_null($user)) {
$password = Str::password();
if ($invitation->exists()) { $user = User::create([
$created_at = $invitation->first()->created_at; 'name' => Str::of($this->email)->before('@'),
$diff = $created_at->diffInMinutes(now()); 'email' => $this->email,
if ($diff <= config('constants.invitation.link.expiration')) { 'password' => Hash::make($password),
return general_error_handler(that: $this, customErrorMessage: "Invitation already sent to $this->email and waiting for action."); 'force_password_reset' => true,
]);
$token = Crypt::encryptString("{$user->email}@@@$password");
$link = route('auth.link', ['token' => $token]);
}
$invitation = TeamInvitation::whereEmail($this->email)->first();
if (!is_null($invitation)) {
$invitationValid = $invitation->isValid();
if ($invitationValid) {
return general_error_handler(that: $this, customErrorMessage: "Pending invitation already exists for $this->email.");
} else { } else {
$invitation->delete(); $invitation->delete();
} }
} }
TeamInvitation::firstOrCreate([ $invitation = TeamInvitation::firstOrCreate([
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'uuid' => $uuid, 'uuid' => $uuid,
'email' => $this->email, 'email' => $this->email,
'role' => $this->role, 'role' => $this->role,
'link' => $link, 'link' => $link,
'via' => $isEmail ? 'email' : 'link', 'via' => $sendEmail ? 'email' : 'link',
]); ]);
if ($isEmail) { if ($sendEmail) {
$user->first()->notify(new InvitationLink); $mail = new MailMessage();
$mail->view('emails.invitation-link', [
'team' => currentTeam()->name,
'invitation_link' => $link,
]);
$mail->subject('You have been invited to ' . currentTeam()->name . ' on ' . config('app.name') . '.');
send_user_an_email($mail, $this->email);
$this->emit('success', 'Invitation sent via email successfully.'); $this->emit('success', 'Invitation sent via email successfully.');
$this->emit('refreshInvitations');
return;
} else { } else {
$this->emit('success', 'Invitation link generated.'); $this->emit('success', 'Invitation link generated.');
$this->emit('refreshInvitations');
} }
$this->emit('refreshInvitations');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$error_message = $e->getMessage(); $error_message = $e->getMessage();
if ($e->getCode() === '23505') { if ($e->getCode() === '23505') {
@@ -75,9 +94,4 @@ class InviteLink extends Component
return general_error_handler(err: $e, that: $this, customErrorMessage: $error_message); return general_error_handler(err: $e, that: $this, customErrorMessage: $error_message);
} }
} }
public function viaLink()
{
$this->generate_invite_link();
}
} }

View File

@@ -3,6 +3,7 @@
namespace App\Http\Livewire\Team; namespace App\Http\Livewire\Team;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Livewire\Component; use Livewire\Component;
class Member extends Component class Member extends Component
@@ -24,6 +25,10 @@ class Member extends Component
public function remove() public function remove()
{ {
$this->member->teams()->detach(currentTeam()); $this->member->teams()->detach(currentTeam());
Cache::forget("team:{$this->member->id}");
Cache::remember('team:' . $this->member->id, 3600, function() {
return $this->member->teams()->first();
});
$this->emit('reloadWindow'); $this->emit('reloadWindow');
} }
} }

View File

@@ -6,6 +6,7 @@ use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\User; use App\Models\User;
use App\Models\Waitlist; use App\Models\Waitlist;
use Livewire\Component; use Livewire\Component;
use Illuminate\Support\Str;
class Index extends Component class Index extends Component
{ {
@@ -46,7 +47,7 @@ class Index extends Component
return; return;
} }
$waitlist = Waitlist::create([ $waitlist = Waitlist::create([
'email' => $this->email, 'email' => Str::lower($this->email),
'type' => 'registration', 'type' => 'registration',
]); ]);

View File

@@ -5,6 +5,7 @@ namespace App\Http\Middleware;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Str;
class IsBoardingFlow class IsBoardingFlow
{ {
@@ -17,6 +18,9 @@ class IsBoardingFlow
{ {
// ray()->showQueries()->color('orange'); // ray()->showQueries()->color('orange');
if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) { if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
if (Str::startsWith($request->path(), 'invitations')) {
return $next($request);
}
return redirect('boarding'); return redirect('boarding');
} }
return $next($request); return $next($request);

View File

@@ -5,6 +5,7 @@ namespace App\Http\Middleware;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Str;
class IsSubscriptionValid class IsSubscriptionValid
{ {
@@ -31,6 +32,9 @@ class IsSubscriptionValid
if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) { if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) {
// ray('SubscriptionValid Middleware'); // ray('SubscriptionValid Middleware');
if (!in_array($request->path(), allowedPathsForUnsubscribedAccounts())) { if (!in_array($request->path(), allowedPathsForUnsubscribedAccounts())) {
if (Str::startsWith($request->path(), 'invitations')) {
return $next($request);
}
return redirect('subscription'); return redirect('subscription');
} else { } else {
return $next($request); return $next($request);

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Notifications\Application\StatusChanged;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ApplicationContainerStatusJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public string $containerName;
public function __construct(
public Application $application,
public int $pullRequestId = 0)
{
$this->containerName = generateApplicationContainerName($application->uuid, $pullRequestId);
}
public function uniqueId(): string
{
return $this->containerName;
}
public function handle(): void
{
try {
$status = getApplicationContainerStatus(application: $this->application);
if ($this->application->status === 'running' && $status !== 'running') {
// $this->application->environment->project->team->notify(new StatusChanged($this->application));
}
if ($this->pullRequestId !== 0) {
$preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pullRequestId);
$preview->status = $status;
$preview->save();
} else {
$this->application->status = $status;
$this->application->save();
}
} catch (\Throwable $e) {
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -17,10 +17,10 @@ use App\Notifications\Application\DeploymentSuccess;
use App\Traits\ExecuteRemoteCommand; use App\Traits\ExecuteRemoteCommand;
use Exception; use Exception;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -28,10 +28,8 @@ use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Throwable; use Throwable;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
use Yosymfony\Toml\Toml;
use Yosymfony\Toml\TomlArray;
class ApplicationDeploymentJob implements ShouldQueue class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
@@ -49,7 +47,6 @@ class ApplicationDeploymentJob implements ShouldQueue
private GithubApp|GitlabApp $source; private GithubApp|GitlabApp $source;
private StandaloneDocker|SwarmDocker $destination; private StandaloneDocker|SwarmDocker $destination;
private Server $server; private Server $server;
private string $private_key_location;
private ApplicationPreview|null $preview = null; private ApplicationPreview|null $preview = null;
private string $container_name; private string $container_name;
@@ -92,7 +89,7 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->container_name = generateApplicationContainerName($this->application->uuid, $this->pull_request_id); $this->container_name = generateApplicationContainerName($this->application->uuid, $this->pull_request_id);
$this->private_key_location = save_private_key_for_server($this->server); addPrivateKeyToSshAgent($this->server);
$this->saved_outputs = collect(); $this->saved_outputs = collect();
// Set preview fqdn // Set preview fqdn
@@ -122,6 +119,9 @@ class ApplicationDeploymentJob implements ShouldQueue
if ($containers->count() > 0) { if ($containers->count() > 0) {
$this->currently_running_container_name = data_get($containers[0], 'Names'); $this->currently_running_container_name = data_get($containers[0], 'Names');
} }
if ($this->pull_request_id !== 0 && $this->pull_request_id !== null) {
$this->currently_running_container_name = $this->container_name;
}
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]); ]);
@@ -135,7 +135,9 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->deploy(); $this->deploy();
} }
} }
if ($this->application->fqdn) dispatch(new ProxyContainerStatusJob($this->server)); if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value); $this->next(ApplicationDeploymentStatus::FINISHED->value);
} catch (Exception $e) { } catch (Exception $e) {
ray($e); ray($e);
@@ -270,6 +272,7 @@ class ApplicationDeploymentJob implements ShouldQueue
"echo 'Rolling update completed.'" "echo 'Rolling update completed.'"
], ],
); );
$this->application->update(['status' => 'running']);
break; break;
} }
$counter++; $counter++;
@@ -296,7 +299,11 @@ class ApplicationDeploymentJob implements ShouldQueue
// $this->generate_build_env_variables(); // $this->generate_build_env_variables();
// $this->add_build_env_variables_to_dockerfile(); // $this->add_build_env_variables_to_dockerfile();
$this->build_image(); $this->build_image();
$this->rolling_update(); $this->stop_running_container();
$this->execute_remote_command(
["echo -n 'Starting preview deployment.'"],
[$this->execute_in_builder("docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true],
);
} }
private function prepare_builder_image() private function prepare_builder_image()
@@ -576,10 +583,15 @@ class ApplicationDeploymentJob implements ShouldQueue
private function set_labels_for_applications() private function set_labels_for_applications()
{ {
$appId = $this->application->id;
if ($this->pull_request_id !== 0) {
$appId = $appId . '-pr-' . $this->pull_request_id;
}
$labels = []; $labels = [];
$labels[] = 'coolify.managed=true'; $labels[] = 'coolify.managed=true';
$labels[] = 'coolify.version=' . config('version'); $labels[] = 'coolify.version=' . config('version');
$labels[] = 'coolify.applicationId=' . $this->application->id; $labels[] = 'coolify.applicationId=' . $appId;
$labels[] = 'coolify.type=application'; $labels[] = 'coolify.type=application';
$labels[] = 'coolify.name=' . $this->application->name; $labels[] = 'coolify.name=' . $this->application->name;
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
@@ -640,7 +652,7 @@ class ApplicationDeploymentJob implements ShouldQueue
private function generate_healthcheck_commands() private function generate_healthcheck_commands()
{ {
if ($this->application->dockerfile) { if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile') {
// TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl. // TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl.
return 'exit 0'; return 'exit 0';
} }

View File

@@ -6,12 +6,13 @@ use App\Enums\ProcessStatus;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class ApplicationPullRequestUpdateJob implements ShouldQueue class ApplicationPullRequestUpdateJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -4,12 +4,13 @@ namespace App\Jobs;
use App\Actions\License\CheckResaleLicense; use App\Actions\License\CheckResaleLicense;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class CheckResaleLicenseJob implements ShouldQueue class CheckResaleLicenseJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -2,15 +2,17 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\TeamInvitation;
use App\Models\Waitlist; use App\Models\Waitlist;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@@ -31,7 +33,12 @@ class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('CleanupInstanceStuffsJob failed with error: ' . $e->getMessage()); send_internal_notification('CleanupInstanceStuffsJob failed with error: ' . $e->getMessage());
ray($e->getMessage()); ray($e->getMessage());
throw $e; }
try {
$this->cleanup_invitation_link();
} catch (\Throwable $e) {
send_internal_notification('CleanupInstanceStuffsJob failed with error: ' . $e->getMessage());
ray($e->getMessage());
} }
} }
@@ -42,4 +49,11 @@ class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique
$item->delete(); $item->delete();
} }
} }
private function cleanup_invitation_link()
{
$invitation = TeamInvitation::all();
foreach ($invitation as $item) {
$item->isValid();
}
}
} }

View File

@@ -0,0 +1,290 @@
<?php
namespace App\Jobs;
use App\Actions\Proxy\StartProxy;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Notifications\Container\ContainerRestarted;
use App\Notifications\Container\ContainerStopped;
use App\Notifications\Server\Unreachable;
use Arr;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 120;
public function __construct(public Server $server)
{
}
public function middleware(): array
{
return [new WithoutOverlapping($this->server->uuid)];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
private function checkServerConnection() {
ray("Checking server connection to {$this->server->ip}");
$uptime = instant_remote_process(['uptime'], $this->server, false);
if (!is_null($uptime)) {
ray('Server is up');
return true;
}
}
public function handle(): void
{
try {
ray()->clearAll();
$serverUptimeCheckNumber = 0;
$serverUptimeCheckNumberMax = 5;
while (true) {
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
$this->server->settings()->update(['is_reachable' => false]);
$this->server->team->notify(new Unreachable($this->server));
return;
}
$result = $this->checkServerConnection();
if ($result) {
break;
}
$serverUptimeCheckNumber++;
sleep(5);
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
$containers = format_docker_command_output_to_json($containers);
$applications = $this->server->applications();
$databases = $this->server->databases();
$previews = $this->server->previews();
if ($this->server->isProxyShouldRun()) {
$foundProxyContainer = $containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-proxy';
})->first();
if (!$foundProxyContainer) {
resolve(StartProxy::class)($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));
}
}
$foundApplications = [];
$foundApplicationPreviews = [];
$foundDatabases = [];
foreach ($containers as $container) {
$containerStatus = data_get($container, 'State.Status');
$labels = data_get($container, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels));
$labelId = data_get($labels, 'coolify.applicationId');
if ($labelId) {
if (str_contains($labelId,'-pr-')) {
$previewId = (int) Str::after($labelId, '-pr-');
$applicationId = (int) Str::before($labelId, '-pr-');
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id',$previewId)->first();
if ($preview) {
$foundApplicationPreviews[] = $preview->id;
$statusFromDb = $preview->status;
if ($statusFromDb !== $containerStatus) {
$preview->update(['status' => $containerStatus]);
}
} else {
//Notify user that this container should not be there.
}
} else {
$application = $applications->where('id', $labelId)->first();
if ($application) {
$foundApplications[] = $application->id;
$statusFromDb = $application->status;
if ($statusFromDb !== $containerStatus) {
$application->update(['status' => $containerStatus]);
}
} else {
//Notify user that this container should not be there.
}
}
} else {
$uuid = data_get($labels, 'com.docker.compose.service');
if ($uuid) {
$database = $databases->where('uuid', $uuid)->first();
if ($database) {
$foundDatabases[] = $database->id;
$statusFromDb = $database->status;
if ($statusFromDb !== $containerStatus) {
$database->update(['status' => $containerStatus]);
}
} else {
// Notify user that this container should not be there.
}
}
}
}
$notRunningApplications = $applications->pluck('id')->diff($foundApplications);
foreach($notRunningApplications as $applicationId) {
$application = $applications->where('id', $applicationId)->first();
if ($application->status === 'exited') {
continue;
}
$application->update(['status' => 'exited']);
$name = data_get($application, 'name');
$fqdn = data_get($application, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$project = data_get($application, 'environment.project');
$environment = data_get($application, 'environment');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/application/" . $application->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
foreach ($notRunningApplicationPreviews as $previewId) {
$preview = $previews->where('id', $previewId)->first();
if ($preview->status === 'exited') {
continue;
}
$preview->update(['status' => 'exited']);
$name = data_get($preview, 'name');
$fqdn = data_get($preview, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$project = data_get($preview, 'application.environment.project');
$environment = data_get($preview, 'application.environment');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/application/" . $preview->application->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
foreach($notRunningDatabases as $database) {
$database = $databases->where('id', $database)->first();
if ($database->status === 'exited') {
continue;
}
$database->update(['status' => 'exited']);
$name = data_get($database, 'name');
$fqdn = data_get($database, 'fqdn');
$containerName = $name;
$project = data_get($database, 'environment.project');
$environment = data_get($database, 'environment');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/database/" . $database->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
return;
foreach ($applications as $application) {
$uuid = data_get($application, 'uuid');
$id = data_get($application, 'id');
$foundContainer = $containers->filter(function ($value, $key) use ($id, $uuid) {
$labels = data_get($value, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels));
$labelId = data_get($labels, 'coolify.applicationId');
if ($labelId == $id) {
return $value;
}
$isPR = Str::startsWith(data_get($value, 'Name'), "/$uuid");
$isPR = Str::contains(data_get($value, 'Name'), "-pr-");
if ($isPR) {
ray('is pr');
return false;
}
return $value;
})->first();
ray($foundContainer);
if ($foundContainer) {
$containerStatus = data_get($foundContainer, 'State.Status');
$databaseStatus = data_get($application, 'status');
if ($containerStatus !== $databaseStatus) {
$application->update(['status' => $containerStatus]);
}
} else {
$databaseStatus = data_get($application, 'status');
if ($databaseStatus !== 'exited') {
$application->update(['status' => 'exited']);
$name = data_get($application, 'name');
$fqdn = data_get($application, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$project = data_get($application, 'environment.project');
$environment = data_get($application, 'environment');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/application/" . $application->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
}
$previews = $application->previews;
foreach ($previews as $preview) {
$foundContainer = $containers->filter(function ($value, $key) use ($id, $uuid, $preview) {
$labels = data_get($value, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels));
$labelId = data_get($labels, 'coolify.applicationId');
if ($labelId == "$id-pr-{$preview->id}") {
return $value;
}
return Str::startsWith(data_get($value, 'Name'), "/$uuid-pr-{$preview->id}");
})->first();
}
}
foreach ($databases as $database) {
$uuid = data_get($database, 'uuid');
$foundContainer = $containers->filter(function ($value, $key) use ($uuid) {
return Str::startsWith(data_get($value, 'Name'), "/$uuid");
})->first();
if ($foundContainer) {
$containerStatus = data_get($foundContainer, 'State.Status');
$databaseStatus = data_get($database, 'status');
if ($containerStatus !== $databaseStatus) {
$database->update(['status' => $containerStatus]);
}
} else {
$databaseStatus = data_get($database, 'status');
if ($databaseStatus !== 'exited') {
$database->update(['status' => 'exited']);
$name = data_get($database, 'name');
$containerName = $name;
$project = data_get($database, 'environment.project');
$environment = data_get($database, 'environment');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/database/" . $database->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
}
}
}
// TODO Monitor other containers not managed by Coolify
} catch (\Throwable $e) {
send_internal_notification('ContainerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -4,13 +4,14 @@ namespace App\Jobs;
use App\Actions\CoolifyTask\RunRemoteProcess; use App\Actions\CoolifyTask\RunRemoteProcess;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
class CoolifyTask implements ShouldQueue class CoolifyTask implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -12,6 +12,7 @@ use App\Notifications\Database\BackupFailed;
use App\Notifications\Database\BackupSuccess; use App\Notifications\Database\BackupSuccess;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
@@ -20,7 +21,7 @@ use Illuminate\Queue\SerializesModels;
use Throwable; use Throwable;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class DatabaseBackupJob implements ShouldQueue class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -1,56 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\ApplicationPreview;
use App\Models\StandalonePostgresql;
use App\Notifications\Application\StatusChanged;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class DatabaseContainerStatusJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public string $containerName;
public function __construct(
public StandalonePostgresql $database,
) {
$this->containerName = $database->uuid;
}
public function uniqueId(): string
{
return $this->containerName;
}
public function handle(): void
{
try {
$status = getContainerStatus(
server: $this->database->destination->server,
container_id: $this->containerName,
throwError: false
);
if ($this->database->status === 'running' && $status !== 'running') {
if (data_get($this->database, 'environment.project.team')) {
// $this->database->environment->project->team->notify(new StatusChanged($this->database));
}
}
if ($this->database->status !== $status) {
$this->database->status = $status;
$this->database->save();
}
} catch (\Throwable $e) {
send_internal_notification('DatabaseContainerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -5,13 +5,14 @@ namespace App\Jobs;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Server; use App\Models\Server;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class DockerCleanupJob implements ShouldQueue class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -4,13 +4,14 @@ namespace App\Jobs;
use App\Actions\Server\UpdateCoolify; use App\Actions\Server\UpdateCoolify;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class InstanceAutoUpdateJob implements ShouldQueue, ShouldBeUnique class InstanceAutoUpdateJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -1,86 +0,0 @@
<?php
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\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ProxyContainerStatusJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public Server $server;
public $tries = 1;
public $timeout = 120;
public function __construct(Server $server)
{
$this->server = $server;
}
public function middleware(): array
{
return [new WithoutOverlapping($this->server->uuid)];
}
public function uniqueId(): string
{
ray($this->server->uuid);
return $this->server->uuid;
}
public function handle(): void
{
try {
$proxyType = data_get($this->server, 'proxy.type');
if ($proxyType === ProxyTypes::NONE->value) {
return;
}
if (is_null($proxyType)) {
if ($this->server->isProxyShouldRun()) {
$this->server->proxy->type = ProxyTypes::TRAEFIK_V2->value;
$this->server->proxy->status = ProxyStatus::EXITED->value;
$this->server->save();
resolve(StartProxy::class)($this->server);
return;
}
}
$container = getContainerStatus(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: false);
$containerStatus = data_get($container, 'State.Status');
$databaseContainerStatus = data_get($this->server, 'proxy.status', 'exited');
if ($proxyType !== ProxyTypes::NONE->value) {
if ($containerStatus === 'running') {
$this->server->proxy->status = $containerStatus;
$this->server->save();
return;
}
if ((is_null($containerStatus) ||$containerStatus !== 'running' || $databaseContainerStatus !== 'running' || ($containerStatus && $databaseContainerStatus !== $containerStatus)) && $this->server->isProxyShouldRun()) {
$this->server->proxy->status = $containerStatus;
$this->server->save();
resolve(StartProxy::class)($this->server);
return;
}
}
} catch (\Throwable $e) {
if ($e->getCode() === 1) {
$this->server->proxy->status = 'exited';
$this->server->save();
}
send_internal_notification('ProxyContainerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Jobs;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Waitlist; use App\Models\Waitlist;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
@@ -13,7 +14,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
class SendConfirmationForWaitlistJob implements ShouldQueue class SendConfirmationForWaitlistJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -3,13 +3,14 @@
namespace App\Jobs; namespace App\Jobs;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
class SendMessageToDiscordJob implements ShouldQueue class SendMessageToDiscordJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -3,14 +3,15 @@
namespace App\Jobs; namespace App\Jobs;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Str; use Illuminate\Support\Str;
class SendMessageToTelegramJob implements ShouldQueue class SendMessageToTelegramJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -1,79 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Str;
class ServerDetailsCheckJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 120;
public function __construct(public Server $server)
{
}
public function middleware(): array
{
return [new WithoutOverlapping($this->server->uuid)];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
public function handle(): void
{
try {
ray()->clearAll();
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
$containers = format_docker_command_output_to_json($containers);
$applications = $this->server->applications();
// ray($applications);
// ray(format_docker_command_output_to_json($containers));
foreach ($applications as $application) {
$uuid = data_get($application, 'uuid');
$foundContainer = $containers->filter(function ($value, $key) use ($uuid) {
$image = data_get($value, 'Config.Image');
return Str::startsWith($image, $uuid);
})->first();
if ($foundContainer) {
$containerStatus = data_get($foundContainer, 'State.Status');
$databaseStatus = data_get($application, 'status');
ray($containerStatus, $databaseStatus);
if ($containerStatus !== $databaseStatus) {
// $application->update(['status' => $containerStatus]);
}
}
}
// foreach ($containers as $container) {
// $labels = format_docker_labels_to_json(data_get($container,'Config.Labels'));
// $foundLabel = $labels->filter(fn ($value, $key) => Str::startsWith($key, 'coolify.applicationId'));
// if ($foundLabel->count() > 0) {
// $appFound = $applications->where('id', $foundLabel['coolify.applicationId'])->first();
// if ($appFound) {
// $containerStatus = data_get($container, 'State.Status');
// $databaseStatus = data_get($appFound, 'status');
// ray($containerStatus, $databaseStatus);
// }
// }
// }
} catch (\Throwable $e) {
// send_internal_notification('ServerDetailsCheckJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -4,13 +4,14 @@ namespace App\Jobs;
use App\Models\Team; use App\Models\Team;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class SubscriptionInvoiceFailedJob implements ShouldQueue class SubscriptionInvoiceFailedJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -4,13 +4,14 @@ namespace App\Jobs;
use App\Models\Team; use App\Models\Team;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class SubscriptionTrialEndedJob implements ShouldQueue class SubscriptionTrialEndedJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -4,13 +4,14 @@ namespace App\Jobs;
use App\Models\Team; use App\Models\Team;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class SubscriptionTrialEndsSoonJob implements ShouldQueue class SubscriptionTrialEndsSoonJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

View File

@@ -105,6 +105,14 @@ class Server extends BaseModel
})->flatten(); })->flatten();
} }
public function previews() {
return $this->destinations()->map(function ($standaloneDocker) {
return $standaloneDocker->applications->map(function ($application) {
return $application->previews;
})->flatten();
})->flatten();
}
public function destinations() public function destinations()
{ {
$standalone_docker = $this->hasMany(StandaloneDocker::class)->get(); $standalone_docker = $this->hasMany(StandaloneDocker::class)->get();

View File

@@ -19,6 +19,13 @@ class Team extends Model implements SendsDiscord, SendsEmail
'resend_api_key' => 'encrypted', 'resend_api_key' => 'encrypted',
]; ];
protected static function booted()
{
static::saved(function () {
refreshSession();
});
}
public function routeNotificationForDiscord() public function routeNotificationForDiscord()
{ {
return data_get($this, 'discord_webhook_url', null); return data_get($this, 'discord_webhook_url', null);

View File

@@ -19,4 +19,13 @@ class TeamInvitation extends Model
{ {
return $this->belongsTo(Team::class); return $this->belongsTo(Team::class);
} }
public function isValid() {
$createdAt = $this->created_at;
$diff = $createdAt->diffInMinutes(now());
if ($diff <= config('constants.invitation.link.expiration')) {
return true;
} else {
$this->delete();
}
}
} }

View File

@@ -4,10 +4,10 @@ namespace App\Models;
use App\Notifications\Channels\SendsEmail; use App\Notifications\Channels\SendsEmail;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword; use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
use Cache;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Cache;
use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
@@ -61,7 +61,7 @@ class User extends Authenticatable implements SendsEmail
public function isAdmin() public function isAdmin()
{ {
return $this->pivot->role === 'admin' || $this->pivot->role === 'owner'; return data_get($this->pivot,'role') === 'admin' || data_get($this->pivot,'role') === 'owner';
} }
public function isAdminFromSession() public function isAdminFromSession()
@@ -78,7 +78,8 @@ class User extends Authenticatable implements SendsEmail
if ($is_part_of_root_team && $is_admin_of_root_team) { if ($is_part_of_root_team && $is_admin_of_root_team) {
return true; return true;
} }
$role = $teams->where('id', auth()->user()->id)->first()->pivot->role; $team = $teams->where('id', session('currentTeam')->id)->first();
$role = data_get($team,'pivot.role');
return $role === 'admin' || $role === 'owner'; return $role === 'admin' || $role === 'owner';
} }

View File

@@ -53,7 +53,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
$pull_request_id = data_get($this->preview, 'pull_request_id', 0); $pull_request_id = data_get($this->preview, 'pull_request_id', 0);
$fqdn = $this->fqdn; $fqdn = $this->fqdn;
if ($pull_request_id === 0) { if ($pull_request_id === 0) {
$mail->subject("✅New version is deployed of {$this->application_name}"); $mail->subject(" New version is deployed of {$this->application_name}");
} else { } else {
$fqdn = $this->preview->fqdn; $fqdn = $this->preview->fqdn;
$mail->subject("✅ Pull request #{$pull_request_id} of {$this->application_name} deployed successfully"); $mail->subject("✅ Pull request #{$pull_request_id} of {$this->application_name} deployed successfully");

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Notifications\Container;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ContainerRestarted extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public function __construct(public string $name, public Server $server, public ?string $url = null)
{
}
public function via(object $notifiable): array
{
return setNotificationChannels($notifiable, 'status_changes');
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("✅ Container ({$this->name}) has been restarted automatically on {$this->server->name}");
$mail->view('emails.container-restarted', [
'containerName' => $this->name,
'serverName' => $this->server->name,
'url' => $this->url ,
]);
return $mail;
}
public function toDiscord(): string
{
$message = "✅ Container ({$this->name}) has been restarted automatically on {$this->server->name}";
return $message;
}
public function toTelegram(): array
{
$message = "✅ Container ({$this->name}) has been restarted automatically on {$this->server->name}";
$payload = [
"message" => $message,
];
if ($this->url) {
$payload['buttons'] = [
[
[
"text" => "Check Proxy in Coolify",
"url" => $this->url
]
]
];
};
return $payload;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Notifications\Container;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ContainerStopped extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public string $name, public Server $server, public ?string $url = null)
{
}
public function via(object $notifiable): array
{
return setNotificationChannels($notifiable, 'status_changes');
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("⛔ Container {$this->name} has been stopped on {$this->server->name}");
$mail->view('emails.container-stopped', [
'containerName' => $this->name,
'serverName' => $this->server->name,
'url' => $this->url,
]);
return $mail;
}
public function toDiscord(): string
{
$message = "⛔ Container {$this->name} has been stopped on {$this->server->name}";
return $message;
}
public function toTelegram(): array
{
$message = "⛔ Container ($this->name} has been stopped on {$this->server->name}";
$payload = [
"message" => $message,
];
if ($this->url) {
$payload['buttons'] = [
[
[
"text" => "Open Application in Coolify",
"url" => $this->url
]
]
];
}
return $payload;
}
}

View File

@@ -1,53 +0,0 @@
<?php
namespace App\Notifications\Server;
use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Str;
class NotReachable extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public function __construct(public Server $server)
{
}
public function via(object $notifiable): array
{
return setNotificationChannels($notifiable, 'status_changes');
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
// $fqdn = $this->fqdn;
$mail->subject("⛔ Server '{$this->server->name}' is unreachable");
// $mail->view('emails.application-status-changes', [
// 'name' => $this->application_name,
// 'fqdn' => $fqdn,
// 'application_url' => $this->application_url,
// ]);
return $mail;
}
public function toDiscord(): string
{
$message = '⛔ Server \'' . $this->server->name . '\' is unreachable (could be a temporary issue). If you receive this more than twice in a row, please check your server.';
return $message;
}
public function toTelegram(): array
{
return [
"message" => '⛔ Server \'' . $this->server->name . '\' is unreachable (could be a temporary issue). If you receive this more than twice in a row, please check your server.'
];
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Notifications\Server;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class Unreachable extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public Server $server)
{
}
public function via(object $notifiable): array
{
return setNotificationChannels($notifiable, 'status_changes');
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("⛔ Server ({$this->server->name}) is unreachable after trying to connect to it 5 times");
$mail->view('emails.server-lost-connection', [
'name' => $this->server->name,
]);
return $mail;
}
public function toDiscord(): string
{
$message = "⛔ Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: You have to validate your server again after you fix the issue.";
return $message;
}
public function toTelegram(): array
{
return [
"message" => "⛔ Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: You have to validate your server again after you fix the issue."
];
}
}

View File

@@ -28,9 +28,8 @@ trait ExecuteRemoteCommand
$ip = data_get($this->server, 'ip'); $ip = data_get($this->server, 'ip');
$user = data_get($this->server, 'user'); $user = data_get($this->server, 'user');
$port = data_get($this->server, 'port'); $port = data_get($this->server, 'port');
$private_key_location = get_private_key_for_server($this->server);
$commandsText->each(function ($single_command) use ($private_key_location, $ip, $user, $port) { $commandsText->each(function ($single_command) use ($ip, $user, $port) {
$command = data_get($single_command, 'command') ?? $single_command[0] ?? null; $command = data_get($single_command, 'command') ?? $single_command[0] ?? null;
if ($command === null) { if ($command === null) {
throw new \RuntimeException('Command is not set'); throw new \RuntimeException('Command is not set');
@@ -39,8 +38,8 @@ trait ExecuteRemoteCommand
$ignore_errors = data_get($single_command, 'ignore_errors', false); $ignore_errors = data_get($single_command, 'ignore_errors', false);
$this->save = data_get($single_command, 'save'); $this->save = data_get($single_command, 'save');
$remote_command = generate_ssh_command($private_key_location, $ip, $user, $port, $command); $remote_command = generateSshCommand( $ip, $user, $port, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) { $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) {
$output = Str::of($output)->trim(); $output = Str::of($output)->trim();
$new_log_entry = [ $new_log_entry = [
'command' => $command, 'command' => $command,

View File

@@ -24,6 +24,7 @@ class Textarea extends Component
public bool $disabled = false, public bool $disabled = false,
public bool $readonly = false, public bool $readonly = false,
public string|null $helper = null, public string|null $helper = null,
public bool $realtimeValidation = false,
public string $defaultClass = "textarea bg-coolgray-200 rounded text-white scrollbar disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50" public string $defaultClass = "textarea bg-coolgray-200 rounded text-white scrollbar disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
) { ) {
// //

View File

@@ -75,7 +75,6 @@ function getApplicationContainerStatus(Application $application) {
} }
function getContainerStatus(Server $server, string $container_id, bool $all_data = false, bool $throwError = false) function getContainerStatus(Server $server, string $container_id, bool $all_data = false, bool $throwError = false)
{ {
// check_server_connection($server);
$container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError); $container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError);
if (!$container) { if (!$container) {
return 'exited'; return 'exited';
@@ -91,7 +90,7 @@ function generateApplicationContainerName(string $uuid, int $pull_request_id = 0
{ {
$now = now()->format('Hisu'); $now = now()->format('Hisu');
if ($pull_request_id !== 0 && $pull_request_id !== null) { if ($pull_request_id !== 0 && $pull_request_id !== null) {
return $uuid . '-pr-' . $pull_request_id . '-' . $now; return $uuid . '-pr-' . $pull_request_id;
} else { } else {
return $uuid . '-' . $now; return $uuid . '-' . $now;
} }

View File

@@ -15,6 +15,7 @@ use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Sleep; use Illuminate\Support\Sleep;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str;
function remote_process( function remote_process(
array $command, array $command,
@@ -33,12 +34,9 @@ function remote_process(
} }
} }
$private_key_location = save_private_key_for_server($server);
return resolve(PrepareCoolifyTask::class, [ return resolve(PrepareCoolifyTask::class, [
'remoteProcessArgs' => new CoolifyTaskArgs( 'remoteProcessArgs' => new CoolifyTaskArgs(
server_ip: $server->ip, server_ip: $server->ip,
private_key_location: $private_key_location,
command: <<<EOT command: <<<EOT
{$command_string} {$command_string}
EOT, EOT,
@@ -52,37 +50,48 @@ function remote_process(
])(); ])();
} }
function get_private_key_for_server(Server $server) // function removePrivateKeyFromSshAgent(Server $server)
{ // {
$temp_file = "id.root@{$server->ip}"; // if (data_get($server, 'privateKey.private_key') === null) {
return '/var/www/html/storage/app/ssh/keys/' . $temp_file; // throw new \Exception("Server {$server->name} does not have a private key");
} // }
// // processWithEnv()->run("echo '{$server->privateKey->private_key}' | ssh-add -d -");
function save_private_key_for_server(Server $server) // }
function addPrivateKeyToSshAgent(Server $server)
{ {
if (data_get($server, 'privateKey.private_key') === null) { if (data_get($server, 'privateKey.private_key') === null) {
throw new \Exception("Server {$server->name} does not have a private key"); throw new \Exception("Server {$server->name} does not have a private key");
} }
$temp_file = "id.root@{$server->ip}"; $sshKeyFileLocation = "id.root@{$server->uuid}";
Storage::disk('ssh-keys')->put($temp_file, $server->privateKey->private_key); Storage::disk('ssh-keys')->makeDirectory('.');
Storage::disk('ssh-mux')->makeDirectory('.'); Storage::disk('ssh-mux')->makeDirectory('.');
return '/var/www/html/storage/app/ssh/keys/' . $temp_file; Storage::disk('ssh-keys')->put($sshKeyFileLocation, $server->privateKey->private_key);
return '/var/www/html/storage/app/ssh/keys/' . $sshKeyFileLocation;
} }
function generate_ssh_command(string $private_key_location, string $server_ip, string $user, string $port, string $command, bool $isMux = true) function generateSshCommand(string $server_ip, string $user, string $port, string $command, bool $isMux = true)
{ {
$server = Server::where('ip', $server_ip)->first();
if (!$server) {
throw new \Exception("Server with ip {$server_ip} not found");
}
$privateKeyLocation = addPrivateKeyToSshAgent($server);
$timeout = config('constants.ssh.command_timeout');
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$delimiter = 'EOF-COOLIFY-SSH'; $delimiter = 'EOF-COOLIFY-SSH';
$ssh_command = "ssh "; $ssh_command = "timeout $timeout ssh ";
if ($isMux && config('coolify.mux_enabled')) { if ($isMux && config('coolify.mux_enabled')) {
$ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r '; $ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r ';
} }
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command"; $command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$ssh_command .= "-i {$private_key_location} " $ssh_command .= "-i {$privateKeyLocation} "
. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' . '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
. '-o PasswordAuthentication=no ' . '-o PasswordAuthentication=no '
. '-o ConnectTimeout=3600 ' . "-o ConnectTimeout=$connectionTimeout "
. '-o ServerAliveInterval=20 ' . "-o ServerAliveInterval=$serverInterval "
. '-o RequestTTY=no ' . '-o RequestTTY=no '
. '-o LogLevel=ERROR ' . '-o LogLevel=ERROR '
. "-p {$port} " . "-p {$port} "
@@ -90,26 +99,13 @@ function generate_ssh_command(string $private_key_location, string $server_ip, s
. " 'bash -se' << \\$delimiter" . PHP_EOL . " 'bash -se' << \\$delimiter" . PHP_EOL
. $command . PHP_EOL . $command . PHP_EOL
. $delimiter; . $delimiter;
// ray($ssh_command);
return $ssh_command; return $ssh_command;
} }
function instantCommand(string $command, $throwError = true) {
$process = Process::run($command);
$output = trim($process->output());
$exitCode = $process->exitCode();
if ($exitCode !== 0) {
if (!$throwError) {
return null;
}
throw new \RuntimeException($process->errorOutput(), $exitCode);
}
return $output;
}
function instant_remote_process(array $command, Server $server, $throwError = true, $repeat = 1) function instant_remote_process(array $command, Server $server, $throwError = true, $repeat = 1)
{ {
$command_string = implode("\n", $command); $command_string = implode("\n", $command);
$private_key_location = save_private_key_for_server($server); $ssh_command = generateSshCommand($server->ip, $server->user, $server->port, $command_string);
$ssh_command = generate_ssh_command($private_key_location, $server->ip, $server->user, $server->port, $command_string);
$process = Process::run($ssh_command); $process = Process::run($ssh_command);
$output = trim($process->output()); $output = trim($process->output());
$exitCode = $process->exitCode(); $exitCode = $process->exitCode();
@@ -161,12 +157,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
function refresh_server_connection(PrivateKey $private_key) function refresh_server_connection(PrivateKey $private_key)
{ {
foreach ($private_key->servers as $server) { foreach ($private_key->servers as $server) {
// Delete the old ssh mux file to force a new one to be created
Storage::disk('ssh-mux')->delete($server->muxFilename()); Storage::disk('ssh-mux')->delete($server->muxFilename());
// check if user is authenticated
// if (currentTeam()->id) {
// currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
// }
} }
} }
@@ -208,30 +199,7 @@ function validateServer(Server $server)
$server->settings->is_usable = false; $server->settings->is_usable = false;
throw $e; throw $e;
} finally { } finally {
if(data_get($server,'settings')) $server->settings->save(); if (data_get($server, 'settings')) $server->settings->save();
}
}
function check_server_connection(Server $server)
{
try {
refresh_server_connection($server->privateKey);
instant_remote_process(['uptime'], $server);
$server->unreachable_count = 0;
$server->settings->is_reachable = true;
} catch (\Throwable $e) {
if ($server->unreachable_count == 2) {
$server->team->notify(new NotReachable($server));
$server->settings->is_reachable = false;
$server->settings->save();
} else {
$server->unreachable_count += 1;
}
throw $e;
} finally {
$server->settings->save();
$server->save();
} }
} }

View File

@@ -2,6 +2,7 @@
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Team; use App\Models\Team;
use App\Models\User;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
@@ -10,6 +11,7 @@ use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@@ -60,7 +62,11 @@ function showBoarding(): bool
function refreshSession(?Team $team = null): void function refreshSession(?Team $team = null): void
{ {
if (!$team) { if (!$team) {
$team = Team::find(currentTeam()->id); if (auth()->user()->currentTeam()) {
$team = Team::find(auth()->user()->currentTeam()->id);
} else {
$team = User::find(auth()->user()->id)->teams->first();
}
} }
Cache::forget('team:' . auth()->user()->id); Cache::forget('team:' . auth()->user()->id);
Cache::remember('team:' . auth()->user()->id, 3600, function() use ($team) { Cache::remember('team:' . auth()->user()->id, 3600, function() use ($team) {
@@ -275,6 +281,7 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null
[], [],
fn (Message $message) => $message fn (Message $message) => $message
->to($email) ->to($email)
->replyTo($email)
->cc($cc) ->cc($cc)
->subject($mail->subject) ->subject($mail->subject)
->html((string) $mail->render()) ->html((string) $mail->render())

View File

@@ -56,7 +56,7 @@ function isSubscriptionActive()
} }
$subscription = $team?->subscription; $subscription = $team?->subscription;
if (!$subscription) { if (is_null($subscription)) {
return false; return false;
} }
if (isLemon()) { if (isLemon()) {

View File

@@ -1,5 +1,10 @@
<?php <?php
return [ return [
'ssh' =>[
'connection_timeout' => 10,
'server_interval' => 20,
'command_timeout' => 7200,
],
'waitlist' => [ 'waitlist' => [
'expiration' => 10, 'expiration' => 10,
], ],

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.35', 'release' => '4.0.0-beta.37',
'server_name' => env('APP_ID', 'coolify'), 'server_name' => env('APP_ID', 'coolify'),
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.35'; return '4.0.0-beta.37';

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('team_invitations', function (Blueprint $table) {
$table->text('link')->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('team_invitations', function (Blueprint $table) {
$table->string('link')->change();
});
}
};

View File

@@ -24,4 +24,4 @@ RUN echo "alias mfs='php artisan migrate:fresh --seed'" >>/etc/bash.bashrc
RUN echo "alias cda='composer dump-autoload'" >>/etc/bash.bashrc RUN echo "alias cda='composer dump-autoload'" >>/etc/bash.bashrc
RUN echo "alias run='./scripts/run'" >>/etc/bash.bashrc RUN echo "alias run='./scripts/run'" >>/etc/bash.bashrc
# COPY --chmod=755 docker/dev-ssu/etc/s6-overlay/ /etc/s6-overlay/ COPY --chmod=755 docker/dev-ssu/etc/s6-overlay/ /etc/s6-overlay/

View File

@@ -0,0 +1,5 @@
#!/command/execlineb -P
foreground {
s6-sleep 5
su - webuser -c "php /var/www/html/artisan horizon"
}

View File

@@ -1,2 +0,0 @@
#!/command/execlineb -P
su - webuser -c "php /var/www/html/artisan queue:listen"

View File

@@ -1,2 +1,5 @@
#!/command/execlineb -P #!/command/execlineb -P
su - webuser -c "php /var/www/html/artisan schedule:work" foreground {
s6-sleep 5
su - webuser -c "php /var/www/html/artisan schedule:work"
}

View File

@@ -15,7 +15,7 @@
@if (is_transactional_emails_active()) @if (is_transactional_emails_active())
<form action="/forgot-password" method="POST" class="flex flex-col gap-2"> <form action="/forgot-password" method="POST" class="flex flex-col gap-2">
@csrf @csrf
<x-forms.input required value="test@example.com" type="email" name="email" <x-forms.input required type="email" name="email"
label="{{ __('input.email') }}" autofocus /> label="{{ __('input.email') }}" autofocus />
<x-forms.button type="submit">{{ __('auth.forgot_password_send_email') }}</x-forms.button> <x-forms.button type="submit">{{ __('auth.forgot_password_send_email') }}</x-forms.button>
</form> </form>

View File

@@ -14,12 +14,12 @@
</div> </div>
@endif @endif
</div> </div>
@if($explanation) @isset($explanation)
<div class="col-span-1"> <div class="col-span-1">
<h1 class="pb-8 font-bold">Explanation</h1> <h1 class="pb-8 font-bold">Explanation</h1>
<div class="space-y-4"> <div class="space-y-4">
{{$explanation}} {{$explanation}}
</div> </div>
</div> </div>
@endif @endisset
</div> </div>

View File

@@ -30,9 +30,12 @@
</label> </label>
@endif @endif
<textarea placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }} <textarea placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }}
wire:model.defer={{ $id }} @disabled($disabled) @readonly($readonly) @required($required) @if ($realtimeValidation) wire:model.debounce.500ms="{{ $id }}"
id="{{ $id }}" name="{{ $name }}" name={{ $id }} wire:model.defer={{ $value ?? $id }} @else
wire:dirty.class="input-warning"></textarea> wire:model.defer={{ $value ?? $id }}
wire:dirty.class="input-warning"@endif
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $id }}" name="{{ $name }}"
name={{ $id }} ></textarea>
@error($id) @error($id)
<label class="label"> <label class="label">
<span class="text-red-500 label-text-alt">{{ $message }}</span> <span class="text-red-500 label-text-alt">{{ $message }}</span>

View File

@@ -21,7 +21,8 @@
</label> </label>
</fieldset> </fieldset>
</div> </div>
<div class="py-2 text-center"><span class="font-bold text-warning">{{config('constants.limits.trial_period')}} days trial</span> included on all plans, without credit card details.</div> <div class="py-2 text-center"><span class="font-bold text-warning">{{ config('constants.limits.trial_period') }}
days trial</span> included on all plans, without credit card details.</div>
<div x-show="selected === 'monthly'" class="flex justify-center h-10 mt-3 text-sm leading-6 "> <div x-show="selected === 'monthly'" class="flex justify-center h-10 mt-3 text-sm leading-6 ">
<div>Save <span class="font-bold text-warning">1 month</span> annually with the yearly plans. <div>Save <span class="font-bold text-warning">1 month</span> annually with the yearly plans.
</div> </div>
@@ -289,6 +290,170 @@
</div> </div>
</div> </div>
</div> </div>
<div class="pt-8 pb-12 text-4xl font-bold text-center text-white">Included in all plans</div>
<div class="grid grid-cols-1 gap-10 md:grid-cols-2 gap-y-28">
<div>
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500">
<svg width="512" height="512" class="icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M3 7a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3zm12 13H6a3 3 0 0 1-3-3v-2a3 3 0 0 1 3-3h12M7 8v.01M7 16v.01M20 15l-2 3h3l-2 3" />
</svg>
</div>
<div class="text-2xl font-semibold text-white">Bring Your Own Servers</div>
</div>
<div class="mt-1 text-base leading-7 text-gray-300">
Bring your own server from any cloud providers, or even your own server at home! All you need is SSH
access. You will have full control over your server, and you can even use it for other purposes.
</div>
</div>
<div>
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500">
<svg width="512" height="512" class="icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="white" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path
d="M7 7h10a2 2 0 0 1 2 2v1l1 1v3l-1 1v3a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-3l-1-1v-3l1-1V9a2 2 0 0 1 2-2zm3 9h4" />
<circle cx="8.5" cy="11.5" r=".5" fill="#000000" />
<circle cx="15.5" cy="11.5" r=".5" fill="#000000" />
<path d="M9 7L8 3m7 4l1-4" />
</g>
</svg>
</div>
<div class="text-2xl font-semibold text-white">Server Automations</div>
</div>
<div class="mt-1 text-base leading-7 text-gray-300">
Once you connected your server, Coolify will start managing it and do a
lot of adminstrative tasks for you. You can also write your own scripts to
automate your server<span class="text-warning">*</span>.
</div>
</div>
<div>
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500">
<svg width="512" height="512" viewBox="0 0 24 24" class="icon"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M15 11h2a2 2 0 0 1 2 2v2m0 4a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2h4" />
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 1 0-2 0m-3-5V8m.347-3.631A4 4 0 0 1 16 6M3 3l18 18" />
</g>
</svg>
</div>
<div class="text-2xl font-semibold text-white">No Vendor Lock-in</div>
</div>
<div class="mt-1 text-base leading-7 text-gray-300">
You own your own data. All configurations saved on your own servers, so if
you decide to stop using Coolify, you can still continue to manage your
deployed resources.
</div>
</div>
<div>
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500">
<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" />
<rect x="3" y="4" width="18" height="12" rx="1" />
<path d="M7 20h10" />
<path d="M9 16v4" />
<path d="M15 16v4" />
<path d="M7 10h2l2 3l2 -6l1 3h3" />
</svg>
</div>
<div class="text-2xl font-semibold text-white">Monitoring</div>
</div>
<div class="mt-1 text-base leading-7 text-gray-300">
Coolify will automatically monitor your configured servers and deployed
resources. Notifies you if something goes wrong on your favourite
channels, like Discord, Telegram, via Email and more...
</div>
</div>
<div>
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500">
<svg width="512" height="512" class="icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M6 4h10l4 4v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2" />
<path d="M10 14a2 2 0 1 0 4 0a2 2 0 1 0-4 0m4-10v4H8V4" />
</g>
</svg>
</div>
<div class="text-2xl font-semibold text-white">Automatic Backups</div>
</div>
<div class="mt-1 text-base leading-7 text-gray-300">
We automatically backup your databases to any S3 compatible solution. If
something goes wrong, you can easily restore your data with a few clicks.
</div>
</div>
<div>
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500">
<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" />
<polyline points="5 7 10 12 5 17" />
<line x1="13" y1="17" x2="19" y2="17" />
</svg>
</div>
<div class="text-2xl font-semibold text-white">Powerful API</div>
</div>
<div class="mt-1 text-base leading-7 text-gray-300">
Programatically deploy, query, and manage your servers & resources.
Integrate to your CI/CD pipelines, or build your own custom integrations. <span
class="text-warning">*</span>
</div>
</div>
<div>
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500">
<svg width="512" height="512" class="icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path
d="M4 18a2 2 0 1 0 4 0a2 2 0 1 0-4 0M4 6a2 2 0 1 0 4 0a2 2 0 1 0-4 0m12 12a2 2 0 1 0 4 0a2 2 0 1 0-4 0M6 8v8" />
<path d="M11 6h5a2 2 0 0 1 2 2v8" />
<path d="m14 9l-3-3l3-3" />
</g>
</svg>
</div>
<div class="text-2xl font-semibold text-white">Push to Deploy</div>
</div>
<div class="mt-1 text-base leading-7 text-gray-300">
Git integration is default today. We support hosted (github.com,
gitlab.com<span class="inline-block text-warning">*</span>) or self-hosted<span class="text-warning">*</span>
(Github Enterprise, Gitlab) Git repositories.
</div>
</div>
<div>
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center justify-center w-10 h-10 text-white rounded-lg bg-coolgray-500">
<svg width="512" height="512" class="icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0-4 0m-2 8v-1a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1M15 5a2 2 0 1 0 4 0a2 2 0 0 0-4 0m2 5h2a2 2 0 0 1 2 2v1M5 5a2 2 0 1 0 4 0a2 2 0 0 0-4 0m-2 8v-1a2 2 0 0 1 2-2h2" />
</svg>
</div>
<div class="text-2xl font-semibold text-white">Pull Request Deployments</div>
</div>
<div class="mt-1 text-base leading-7 text-gray-300">
Automagically deploy new commits and pull requests separately to quickly
review contributions and speed up your teamwork!
</div>
</div>
</div>
<div class="pt-20 text-xs">
<span class="text-warning">*</span> Some features are work in progress and will be available soon.
</div>
</div> </div>
@isset($other) @isset($other)
{{ $other }} {{ $other }}

View File

@@ -1,4 +1,5 @@
<div class="pb-6"> <div class="pb-6">
<livewire:server.proxy.modal :server="$server" />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1>Server</h1> <h1>Server</h1>
@if ($server->settings->is_reachable) @if ($server->settings->is_reachable)

View File

@@ -0,0 +1,15 @@
<x-emails.layout>
Container ({{ $containerName }}) has been restarted automatically on {{$serverName}}, because it was stopped unexpected.
@if ($containerName === 'coolify-proxy')
Coolify Proxy should run on your server as you have FQDNs set up in one of your resources.
Note: The proxy should not stop unexpectedly, so please check what is going on your server.
If you don't want to use Coolify Proxy, please remove FQDN from your resources or set Proxy type to Custom(None).
@endif
</x-emails.layout>

View File

@@ -0,0 +1,9 @@
<x-emails.layout>
Container {{ $containerName }} has been stopped unexpected on {{$serverName}}.
@if ($url)
Please check what is going on [here]({{ $url }}).
@endif
</x-emails.layout>

View File

@@ -6,6 +6,5 @@ Please [click here]({{ $invitation_link }}) to accept the invitation.
If you have any questions, please contact the team owner.<br><br> If you have any questions, please contact the team owner.<br><br>
If it was not you who requested this invitation, please ignore this email, or instantly revoke the invitation by clicking [here]({{ $invitation_link }}/revoke). If it was not you who requested this invitation, please ignore this email.
</x-emails.layout> </x-emails.layout>

View File

@@ -1,5 +1,10 @@
<x-emails.layout> <x-emails.layout>
Coolify Cloud cannot connect to your server ({{$name}}). Please check your server and make sure it is running.
Coolify cannot connect to your server ({{$name}}). Please check your server and make sure it is running.
All automations & integrations are turned off!
IMPORTANT: You have to validate your server again after you fix the issue.
If you have any questions, please contact us. If you have any questions, please contact us.

View File

@@ -5,12 +5,34 @@
<h1 class="text-5xl font-bold">Welcome to Coolify</h1> <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> <p class="py-6 text-xl text-center">Let me help you to set the basics.</p>
<div class="flex justify-center "> <div class="flex justify-center ">
<x-forms.button class="justify-center box" wire:click="welcome">Get Started <x-forms.button class="justify-center box" wire:click="$set('currentState','explanation')">Get Started
</x-forms.button> </x-forms.button>
</div> </div>
@endif @endif
</div> </div>
<div> <div>
@if ($currentState === 'explanation')
<x-boarding-step title="What is Coolify?">
<x-slot:question>
Coolify is an all-in-one application to automate tasks on your servers, deploy application with Git
integrations, deploy databases and services, monitor these resources with notifications and alerts
without vendor lock-in
and <a href="https://coolify.io" class="text-white hover:underline">much much more</a>.
<br><br>
<span class="text-xl">
<x-highlighted text="Self-hosting with superpowers!" /></span>
</x-slot:question>
<x-slot:explanation>
<p><x-highlighted text="Task automation:" /> You do not to manage your servers too much. Coolify do it for you.</p>
<p><x-highlighted text="No vendor lock-in:" /> All configurations are stored on your server, so everything works without Coolify (except integrations and automations).</p>
<p><x-highlighted text="Monitoring:" />You will get notified on your favourite platform (Discord, Telegram, Email, etc.) when something goes wrong, or an action needed from your side.</p>
</x-slot:explanation>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:click="explanation">Next
</x-forms.button>
</x-slot:actions>
</x-boarding-step>
@endif
@if ($currentState === 'select-server-type') @if ($currentState === 'select-server-type')
<x-boarding-step title="Server"> <x-boarding-step title="Server">
<x-slot:question> <x-slot:question>
@@ -18,9 +40,11 @@
or on a <x-highlighted text="Remote Server" />? or on a <x-highlighted text="Remote Server" />?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:target="setServerType('localhost')" wire:click="setServerType('localhost')">Localhost <x-forms.button class="justify-center box" wire:target="setServerType('localhost')"
wire:click="setServerType('localhost')">Localhost
</x-forms.button> </x-forms.button>
<x-forms.button class="justify-center box" wire:target="setServerType('remote')" wire:click="setServerType('remote')">Remote Server <x-forms.button class="justify-center box" wire:target="setServerType('remote')"
wire:click="setServerType('remote')">Remote Server
</x-forms.button> </x-forms.button>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
@@ -42,9 +66,11 @@
Do you have your own SSH Private Key? Do you have your own SSH Private Key?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:target="setPrivateKey('own')" wire:click="setPrivateKey('own')">Yes <x-forms.button class="justify-center box" wire:target="setPrivateKey('own')"
wire:click="setPrivateKey('own')">Yes
</x-forms.button> </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 class="justify-center box" wire:target="setPrivateKey('create')"
wire:click="setPrivateKey('create')">No (create one for me)
</x-forms.button> </x-forms.button>
@if (count($privateKeys) > 0) @if (count($privateKeys) > 0)
<form wire:submit.prevent='selectExistingPrivateKey' class="flex flex-col w-full gap-4 pr-10"> <form wire:submit.prevent='selectExistingPrivateKey' class="flex flex-col w-full gap-4 pr-10">
@@ -115,9 +141,10 @@
<x-forms.textarea required placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" label="Private Key" <x-forms.textarea required placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" label="Private Key"
id="privateKey" /> id="privateKey" />
@if ($privateKeyType === 'create') @if ($privateKeyType === 'create')
<x-forms.textarea rows="7" readonly label="Public Key" id="publicKey" /> <x-forms.textarea rows="7" readonly label="Public Key" id="publicKey" />
<span class="font-bold text-warning">ACTION REQUIRED: Copy the 'Public Key' to your server's ~/.ssh/authorized_keys <span class="font-bold text-warning">ACTION REQUIRED: Copy the 'Public Key' to your server's
file.</span> ~/.ssh/authorized_keys
file.</span>
@endif @endif
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
</form> </form>
@@ -182,7 +209,8 @@
Could not find Docker Engine on your server. Do you want me to install it for you? Could not find Docker Engine on your server. Do you want me to install it for you?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:click="installDocker" onclick="installDocker.showModal()"> <x-forms.button class="justify-center box" wire:click="installDocker"
onclick="installDocker.showModal()">
Let's do Let's do
it!</x-forms.button> it!</x-forms.button>
</x-slot:actions> </x-slot:actions>
@@ -233,12 +261,14 @@
@endif @endif
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:click="createNewProject">Let's create a new one!</x-forms.button> <x-forms.button class="justify-center box" wire:click="createNewProject">Let's create a new
one!</x-forms.button>
<div> <div>
@if (count($projects) > 0) @if (count($projects) > 0)
<form wire:submit.prevent='selectExistingProject' <form wire:submit.prevent='selectExistingProject'
class="flex flex-col w-full gap-4 lg:w-96"> class="flex flex-col w-full gap-4 lg:w-96">
<x-forms.select label="Existing projects" class="w-96" id='selectedExistingProject'> <x-forms.select label="Existing projects" class="w-96"
id='selectedExistingProject'>
@foreach ($projects as $project) @foreach ($projects as $project)
<option wire:key="{{ $loop->index }}" value="{{ $project->id }}"> <option wire:key="{{ $loop->index }}" value="{{ $project->id }}">
{{ $project->name }}</option> {{ $project->name }}</option>

View File

@@ -21,7 +21,8 @@
Copy from Instance Settings Copy from Instance Settings
</x-forms.button> </x-forms.button>
@endif @endif
@if (isEmailEnabled($team) && auth()->user()->isAdminFromSession()) @if (isEmailEnabled($team) &&
auth()->user()->isAdminFromSession())
<x-forms.button onclick="sendTestEmail.showModal()" <x-forms.button onclick="sendTestEmail.showModal()"
class="text-white normal-case btn btn-xs no-animation btn-primary"> class="text-white normal-case btn btn-xs no-animation btn-primary">
Send Test Email Send Test Email
@@ -51,61 +52,52 @@
</x-forms.button> </x-forms.button>
</form> </form>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<details class="border rounded collapse border-coolgray-500 collapse-arrow "> <div class="p-4 border border-coolgray-500">
<summary class="text-xl collapse-title"> <h3>SMTP Server</h3>
<div>SMTP Server</div> <div class="w-32">
<div class="w-32"> <x-forms.checkbox instantSave id="team.smtp_enabled" label="Enabled" />
<x-forms.checkbox instantSave id="team.smtp_enabled" label="Enabled" />
</div>
</summary>
<div class="collapse-content">
<form wire:submit.prevent='submit' class="flex flex-col">
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input required id="team.smtp_host" placeholder="smtp.mailgun.org"
label="Host" />
<x-forms.input required id="team.smtp_port" placeholder="587" label="Port" />
<x-forms.input id="team.smtp_encryption" helper="If SMTP uses SSL, set it to 'tls'."
placeholder="tls" label="Encryption" />
</div>
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input id="team.smtp_username" label="SMTP Username" />
<x-forms.input id="team.smtp_password" type="password" label="SMTP Password" />
<x-forms.input id="team.smtp_timeout" helper="Timeout value for sending emails."
label="Timeout" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
</form>
</div> </div>
</details> <form wire:submit.prevent='submit' class="flex flex-col">
<details class="border rounded collapse border-coolgray-500 collapse-arrow"> <div class="flex flex-col gap-4">
<summary class="text-xl collapse-title"> <div class="flex flex-col w-full gap-2 xl:flex-row">
<div>Resend</div> <x-forms.input required id="team.smtp_host" placeholder="smtp.mailgun.org" label="Host" />
<div class="w-32"> <x-forms.input required id="team.smtp_port" placeholder="587" label="Port" />
<x-forms.checkbox instantSave='instantSaveResend' id="team.resend_enabled" label="Enabled" /> <x-forms.input id="team.smtp_encryption" helper="If SMTP uses SSL, set it to 'tls'."
placeholder="tls" label="Encryption" />
</div>
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input id="team.smtp_username" label="SMTP Username" />
<x-forms.input id="team.smtp_password" type="password" label="SMTP Password" />
<x-forms.input id="team.smtp_timeout" helper="Timeout value for sending emails."
label="Timeout" />
</div>
</div> </div>
</summary> <div class="flex justify-end gap-4 pt-6">
<div class="collapse-content"> <x-forms.button type="submit">
<form wire:submit.prevent='submitResend' class="flex flex-col"> Save
<div class="flex flex-col gap-4"> </x-forms.button>
<div class="flex flex-col w-full gap-2 xl:flex-row"> </div>
<x-forms.input required type="password" id="team.resend_api_key" placeholder="API key" </form>
label="API Key" /> </div>
</div> <div class="p-4 border border-coolgray-500">
</div> <h3>Resend</h3>
<div class="flex justify-end gap-4 pt-6"> <div class="w-32">
<x-forms.button type="submit"> <x-forms.checkbox instantSave='instantSaveResend' id="team.resend_enabled" label="Enabled" />
Save
</x-forms.button>
</div>
</form>
</div> </div>
</details> <form wire:submit.prevent='submitResend' class="flex flex-col">
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input required type="password" id="team.resend_api_key" placeholder="API key"
label="API Key" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
</form>
</div>
</div> </div>
@endif @endif
@if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings')) @if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings'))

View File

@@ -4,8 +4,13 @@
<x-forms.input id="name" label="Name" required /> <x-forms.input id="name" label="Name" required />
<x-forms.input id="description" label="Description" /> <x-forms.input id="description" label="Description" />
</div> </div>
<x-forms.textarea id="value" rows="10" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" <x-forms.textarea realtimeValidation id="value" rows="10"
label="Private Key" required /> placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" label="Private Key" required />
<x-forms.button wire:click="generateNewKey">Generate new SSH key for me</x-forms.button>
<x-forms.textarea id="publicKey" rows="6" readonly label="Public Key" />
<span class="font-bold text-warning">ACTION REQUIRED: Copy the 'Public Key' to your server's
~/.ssh/authorized_keys
file.</span>
<x-forms.button type="submit"> <x-forms.button type="submit">
Save Private Key Save Private Key
</x-forms.button> </x-forms.button>

View File

@@ -48,10 +48,10 @@
@endif @endif
</div> </div>
@if ($application->previews->count() > 0) @if ($application->previews->count() > 0)
<h4 class="py-4" wire:poll.10000ms='previewRefresh'>Deployed Previews</h4> <h4 class="py-4">Deployed Previews</h4>
<div class="flex gap-6 "> <div class="flex gap-6 ">
@foreach ($application->previews as $preview) @foreach ($application->previews as $preview)
<div class="flex flex-col p-4 bg-coolgray-200 " x-init="$wire.loadStatus('{{ data_get($preview, 'pull_request_id') }}')"> <div class="flex flex-col p-4 bg-coolgray-200">
<div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} | <div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} |
@if (data_get($preview, 'status') === 'running') @if (data_get($preview, 'status') === 'running')
<x-status.running /> <x-status.running />

View File

@@ -35,11 +35,7 @@
label="Is it part of a Swarm cluster?" /> --}} label="Is it part of a Swarm cluster?" /> --}}
</div> </div>
<div class="flex flex-col w-full gap-2 lg:flex-row"> <div class="flex flex-col w-full gap-2 lg:flex-row">
@if ($server->id === 0) <x-forms.input id="server.ip" label="IP Address" required />
<x-forms.input id="server.ip" label="IP Address" required />
@else
<x-forms.input id="server.ip" label="IP Address" readonly required />
@endif
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input id="server.user" label="User" required /> <x-forms.input id="server.user" label="User" required />
<x-forms.input type="number" id="server.port" label="Port" required /> <x-forms.input type="number" id="server.port" label="Port" required />
@@ -52,15 +48,15 @@
</x-forms.button> </x-forms.button>
@endif @endif
@if ($server->settings->is_reachable && !$server->settings->is_usable && $server->id !== 0) @if ($server->settings->is_reachable && !$server->settings->is_usable && $server->id !== 0)
<x-forms.button wire:poll.2000ms='validateServer' class="mt-8 mb-4 box" onclick="installDocker.showModal()" <x-forms.button class="mt-8 mb-4 box" onclick="installDocker.showModal()" wire:click.prevent='installDocker'
wire:click.prevent='installDocker' isHighlighted> isHighlighted>
Install Docker Engine 24.0 Install Docker Engine 24.0
</x-forms.button> </x-forms.button>
@endif @endif
@if ($server->isFunctional()) @if ($server->isFunctional())
<h3 class="py-4">Settings</h3> <h3 class="py-4">Settings</h3>
<x-forms.input id="cleanup_after_percentage" label="Disk Cleanup threshold (%)" required <x-forms.input id="cleanup_after_percentage" label="Disk Cleanup threshold (%)" required
helper="Disk cleanup job will be executed if disk usage is more than this number." /> helper="Disk cleanup job will be executed if disk usage is more than this number." />
@endif @endif
</form> </form>
<h2 class="pt-4">Danger Zone</h2> <h2 class="pt-4">Danger Zone</h2>

View File

@@ -1,16 +1,6 @@
<div> <div>
@if ($server->isFunctional()) @if ($server->isFunctional())
@if (data_get($server,'proxy.type')) @if (data_get($server,'proxy.type'))
<x-modal submitWireAction="proxyStatusUpdated" modalId="startProxy">
<x-slot:modalBody>
<livewire:activity-monitor header="Proxy Startup Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="startProxy.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
<div x-init="$wire.loadProxyConfiguration"> <div x-init="$wire.loadProxyConfiguration">
@if ($selectedProxy === 'TRAEFIK_V2') @if ($selectedProxy === 'TRAEFIK_V2')
<form wire:submit.prevent='submit'> <form wire:submit.prevent='submit'>

View File

@@ -11,7 +11,7 @@
@if (data_get($server, 'proxy.status') === 'running') @if (data_get($server, 'proxy.status') === 'running')
<div class="flex gap-4"> <div class="flex gap-4">
<button> <button>
<a target="_blank" href="{{ base_url(false) }}:8080"> <a target="_blank" href="http://{{$server->ip}}:8080">
Traefik Dashboard Traefik Dashboard
<x-external-link /> <x-external-link />
</a> </a>

View File

@@ -0,0 +1,12 @@
<div>
<x-modal submitWireAction="proxyStatusUpdated" modalId="startProxy">
<x-slot:modalBody>
<livewire:activity-monitor header="Proxy Startup Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="startProxy.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
</div>

View File

@@ -1,3 +1,4 @@
<div class="flex gap-2" x-init="$wire.getProxyStatus"> <div class="flex gap-2" x-init="$wire.getProxyStatus">
@if ($server->proxy->status === 'running') @if ($server->proxy->status === 'running')
<x-status.running text="Proxy Running" /> <x-status.running text="Proxy Running" />

View File

@@ -1,7 +1,34 @@
<x-layout-subscription> <x-layout-subscription>
@if ($settings->is_resale_license_active) @if ($settings->is_resale_license_active)
<div class="flex justify-center mx-10"> @if (auth()->user()->isAdminFromSession())
<div x-data> <div class="flex justify-center mx-10">
<div x-data>
<div class="flex gap-2">
<h1>Subscription</h1>
<livewire:switch-team />
</div>
<div class="flex items-center pb-8">
<span>Currently active team: <span
class="text-warning">{{ session('currentTeam.name') }}</span></span>
</div>
@if (request()->query->get('cancelled'))
<div class="mb-6 rounded alert alert-error">
<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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Something went wrong with your subscription. Please try again or contact
support.</span>
</div>
@endif
@if (config('subscription.provider') !== null)
<livewire:subscription.pricing-plans />
@endif
</div>
</div>
@else
<div class="flex flex-col justify-center mx-10">
<div class="flex gap-2"> <div class="flex gap-2">
<h1>Subscription</h1> <h1>Subscription</h1>
<livewire:switch-team /> <livewire:switch-team />
@@ -10,22 +37,10 @@
<span>Currently active team: <span <span>Currently active team: <span
class="text-warning">{{ session('currentTeam.name') }}</span></span> class="text-warning">{{ session('currentTeam.name') }}</span></span>
</div> </div>
@if (request()->query->get('cancelled')) <div>You are not an admin or have been removed from this team. If this does not make sense, please <span class="text-white underline cursor-pointer" wire:click="help" onclick="help.showModal()">contact us</span>.</div>
<div class="mb-6 rounded alert alert-error">
<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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Something went wrong with your subscription. Please try again or contact support.</span>
</div>
@endif
@if (config('subscription.provider') !== null)
<livewire:subscription.pricing-plans />
@endif
</div> </div>
</div> @endif
@else @else
<div class="px-10">Resale license is not active. Please contact your instance admin.</div> <div class="px-10" >Resale license is not active. Please contact your instance admin.</div>
@endif @endif
</x-layout-subscription> </x-layout-subscription>

View File

@@ -30,6 +30,10 @@ use Laravel\Fortify\Fortify;
Route::post('/forgot-password', function (Request $request) { Route::post('/forgot-password', function (Request $request) {
if (is_transactional_emails_active()) { if (is_transactional_emails_active()) {
$arrayOfRequest = $request->only(Fortify::email());
$request->merge([
'email' => Str::lower($arrayOfRequest['email']),
]);
$type = set_transanctional_email_settings(); $type = set_transanctional_email_settings();
if (!$type) { if (!$type) {
return response()->json(['message' => 'Transactional emails are not active'], 400); return response()->json(['message' => 'Transactional emails are not active'], 400);

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