commit
7e9f0cc07a
77
app/Console/Commands/InviteFromWaitlist.php
Normal file
77
app/Console/Commands/InviteFromWaitlist.php
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Waitlist;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class InviteFromWaitlist extends Command
|
||||||
|
{
|
||||||
|
public Waitlist|null $next_patient = null;
|
||||||
|
public User|null $new_user = null;
|
||||||
|
public string|null $password = null;
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'app:invite-from-waitlist';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Send invitation to the next user in the waitlist';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first();
|
||||||
|
if ($this->next_patient) {
|
||||||
|
$this->register_user();
|
||||||
|
$this->remove_from_waitlist();
|
||||||
|
$this->send_email();
|
||||||
|
} else {
|
||||||
|
$this->info('No verified user found in the waitlist. 👀');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private function register_user()
|
||||||
|
{
|
||||||
|
$already_registered = User::whereEmail($this->next_patient->email)->first();
|
||||||
|
if (!$already_registered) {
|
||||||
|
$this->password = Str::password();
|
||||||
|
$this->new_user = User::create([
|
||||||
|
'name' => Str::of($this->next_patient->email)->before('@'),
|
||||||
|
'email' => $this->next_patient->email,
|
||||||
|
'password' => Hash::make($this->password),
|
||||||
|
'force_password_reset' => true,
|
||||||
|
]);
|
||||||
|
$this->info("User registered ({$this->next_patient->email}) successfully. 🎉");
|
||||||
|
} else {
|
||||||
|
throw new \Exception('User already registered');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private function remove_from_waitlist()
|
||||||
|
{
|
||||||
|
$this->next_patient->delete();
|
||||||
|
$this->info("User removed from waitlist successfully.");
|
||||||
|
}
|
||||||
|
private function send_email()
|
||||||
|
{
|
||||||
|
$mail = new MailMessage();
|
||||||
|
$mail->view('emails.waitlist-invitation', [
|
||||||
|
'email' => $this->next_patient->email,
|
||||||
|
'password' => $this->password,
|
||||||
|
]);
|
||||||
|
$mail->subject('Congratulations! You are invited to join Coolify Cloud.');
|
||||||
|
send_user_an_email($mail, $this->next_patient->email);
|
||||||
|
$this->info("Email sent successfully. 📧");
|
||||||
|
}
|
||||||
|
}
|
@ -3,13 +3,12 @@
|
|||||||
namespace App\Console;
|
namespace App\Console;
|
||||||
|
|
||||||
use App\Jobs\CheckResaleLicenseJob;
|
use App\Jobs\CheckResaleLicenseJob;
|
||||||
use App\Jobs\CheckResaleLicenseKeys;
|
|
||||||
use App\Jobs\CleanupInstanceStuffsJob;
|
use App\Jobs\CleanupInstanceStuffsJob;
|
||||||
use App\Jobs\DatabaseBackupJob;
|
use App\Jobs\DatabaseBackupJob;
|
||||||
use App\Jobs\DockerCleanupJob;
|
use App\Jobs\DockerCleanupJob;
|
||||||
use App\Jobs\InstanceApplicationsStatusJob;
|
|
||||||
use App\Jobs\InstanceAutoUpdateJob;
|
use App\Jobs\InstanceAutoUpdateJob;
|
||||||
use App\Jobs\ProxyCheckJob;
|
use App\Jobs\ProxyCheckJob;
|
||||||
|
use App\Jobs\ResourceStatusJob;
|
||||||
use App\Models\ScheduledDatabaseBackup;
|
use App\Models\ScheduledDatabaseBackup;
|
||||||
use Illuminate\Console\Scheduling\Schedule;
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||||
@ -21,7 +20,7 @@ class Kernel extends ConsoleKernel
|
|||||||
// $schedule->call(fn() => $this->check_scheduled_backups($schedule))->everyTenSeconds();
|
// $schedule->call(fn() => $this->check_scheduled_backups($schedule))->everyTenSeconds();
|
||||||
if (is_dev()) {
|
if (is_dev()) {
|
||||||
$schedule->command('horizon:snapshot')->everyMinute();
|
$schedule->command('horizon:snapshot')->everyMinute();
|
||||||
$schedule->job(new InstanceApplicationsStatusJob)->everyMinute();
|
$schedule->job(new ResourceStatusJob)->everyMinute();
|
||||||
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
|
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
|
||||||
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
|
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
|
||||||
|
|
||||||
@ -31,7 +30,7 @@ class Kernel extends ConsoleKernel
|
|||||||
} else {
|
} else {
|
||||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||||
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
|
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
|
||||||
$schedule->job(new InstanceApplicationsStatusJob)->everyMinute();
|
$schedule->job(new ResourceStatusJob)->everyMinute();
|
||||||
$schedule->job(new CheckResaleLicenseJob)->hourly();
|
$schedule->job(new CheckResaleLicenseJob)->hourly();
|
||||||
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
|
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
|
||||||
$schedule->job(new DockerCleanupJob)->everyTenMinutes();
|
$schedule->job(new DockerCleanupJob)->everyTenMinutes();
|
||||||
@ -49,7 +48,10 @@ class Kernel extends ConsoleKernel
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
foreach ($scheduled_backups as $scheduled_backup) {
|
foreach ($scheduled_backups as $scheduled_backup) {
|
||||||
if (!$scheduled_backup->enabled) continue;
|
if (!$scheduled_backup->enabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
|
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
|
||||||
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ class Change extends Component
|
|||||||
$this->private_key->private_key .= "\n";
|
$this->private_key->private_key .= "\n";
|
||||||
}
|
}
|
||||||
$this->private_key->save();
|
$this->private_key->save();
|
||||||
refreshPrivateKey($this->private_key);
|
refresh_server_connection($this->private_key);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return general_error_handler(err: $e, that: $this);
|
return general_error_handler(err: $e, that: $this);
|
||||||
}
|
}
|
||||||
|
@ -152,7 +152,7 @@ class General extends Component
|
|||||||
if ($this->application->publish_directory && $this->application->publish_directory !== '/') {
|
if ($this->application->publish_directory && $this->application->publish_directory !== '/') {
|
||||||
$this->application->publish_directory = rtrim($this->application->publish_directory, '/');
|
$this->application->publish_directory = rtrim($this->application->publish_directory, '/');
|
||||||
}
|
}
|
||||||
$this->application->fqdn = $domains->implode(',');
|
$this->application->fqdn = data_get($domains->implode(','), '', null);
|
||||||
$this->application->save();
|
$this->application->save();
|
||||||
$this->emit('success', 'Application settings updated!');
|
$this->emit('success', 'Application settings updated!');
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
@ -17,7 +17,7 @@ class BackupExecution extends Component
|
|||||||
{
|
{
|
||||||
delete_backup_locally($this->execution->filename, $this->execution->scheduledDatabaseBackup->database->destination->server);
|
delete_backup_locally($this->execution->filename, $this->execution->scheduledDatabaseBackup->database->destination->server);
|
||||||
$this->execution->delete();
|
$this->execution->delete();
|
||||||
$this->emit('success', 'Backup execution deleted successfully.');
|
$this->emit('success', 'Backup deleted successfully.');
|
||||||
$this->emit('refreshBackupExecutions');
|
$this->emit('refreshBackupExecutions');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ class PrivateKey extends Component
|
|||||||
$this->server->update([
|
$this->server->update([
|
||||||
'private_key_id' => $private_key_id
|
'private_key_id' => $private_key_id
|
||||||
]);
|
]);
|
||||||
refreshPrivateKey($this->server->privateKey);
|
refresh_server_connection($this->server->privateKey);
|
||||||
$this->server->refresh();
|
$this->server->refresh();
|
||||||
$this->checkConnection();
|
$this->checkConnection();
|
||||||
}
|
}
|
||||||
|
72
app/Http/Livewire/Subscription/Actions.php
Normal file
72
app/Http/Livewire/Subscription/Actions.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Livewire\Subscription;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class Actions extends Component
|
||||||
|
{
|
||||||
|
public function cancel()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$subscription_id = auth()->user()->currentTeam()->subscription->lemon_subscription_id;
|
||||||
|
if (!$subscription_id) {
|
||||||
|
throw new \Exception('No subscription found');
|
||||||
|
}
|
||||||
|
$response = Http::withHeaders([
|
||||||
|
'Accept' => 'application/vnd.api+json',
|
||||||
|
'Content-Type' => 'application/vnd.api+json',
|
||||||
|
'Authorization' => 'Bearer ' . config('coolify.lemon_squeezy_api_key'),
|
||||||
|
])->delete('https://api.lemonsqueezy.com/v1/subscriptions/' . $subscription_id);
|
||||||
|
$json = $response->json();
|
||||||
|
if ($response->failed()) {
|
||||||
|
$error = data_get($json, 'errors.0.status');
|
||||||
|
if ($error === '404') {
|
||||||
|
throw new \Exception('Subscription not found.');
|
||||||
|
}
|
||||||
|
throw new \Exception(data_get($json, 'errors.0.title', 'Something went wrong. Please try again later.'));
|
||||||
|
} else {
|
||||||
|
$this->emit('success', 'Subscription cancelled successfully. Reloading in 5s.');
|
||||||
|
$this->emit('reloadWindow', 5000);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return general_error_handler($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public function resume()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$subscription_id = auth()->user()->currentTeam()->subscription->lemon_subscription_id;
|
||||||
|
if (!$subscription_id) {
|
||||||
|
throw new \Exception('No subscription found');
|
||||||
|
}
|
||||||
|
$response = Http::withHeaders([
|
||||||
|
'Accept' => 'application/vnd.api+json',
|
||||||
|
'Content-Type' => 'application/vnd.api+json',
|
||||||
|
'Authorization' => 'Bearer ' . config('coolify.lemon_squeezy_api_key'),
|
||||||
|
])->patch('https://api.lemonsqueezy.com/v1/subscriptions/' . $subscription_id, [
|
||||||
|
'data' => [
|
||||||
|
'type' => 'subscriptions',
|
||||||
|
'id' => $subscription_id,
|
||||||
|
'attributes' => [
|
||||||
|
'cancelled' => false,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$json = $response->json();
|
||||||
|
if ($response->failed()) {
|
||||||
|
$error = data_get($json, 'errors.0.status');
|
||||||
|
if ($error === '404') {
|
||||||
|
throw new \Exception('Subscription not found.');
|
||||||
|
}
|
||||||
|
throw new \Exception(data_get($json, 'errors.0.title', 'Something went wrong. Please try again later.'));
|
||||||
|
} else {
|
||||||
|
$this->emit('success', 'Subscription resumed successfully. Reloading in 5s.');
|
||||||
|
$this->emit('reloadWindow', 5000);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return general_error_handler($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,7 @@ class Waitlist extends Component
|
|||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
if (is_dev()) {
|
if (is_dev()) {
|
||||||
$this->email = 'test@example.com';
|
$this->email = 'waitlist@example.com';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public function submit()
|
public function submit()
|
||||||
@ -27,8 +27,7 @@ class Waitlist extends Component
|
|||||||
try {
|
try {
|
||||||
$already_registered = User::whereEmail($this->email)->first();
|
$already_registered = User::whereEmail($this->email)->first();
|
||||||
if ($already_registered) {
|
if ($already_registered) {
|
||||||
$this->emit('success', 'You are already registered (Thank you 💜).');
|
throw new \Exception('You are already on the waitlist or registered. <br>Please check your email to verify your email address or contact support.');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
$found = ModelsWaitlist::where('email', $this->email)->first();
|
$found = ModelsWaitlist::where('email', $this->email)->first();
|
||||||
if ($found) {
|
if ($found) {
|
||||||
@ -36,7 +35,7 @@ class Waitlist extends Component
|
|||||||
$this->emit('error', 'You are already on the waitlist. <br>Please check your email to verify your email address.');
|
$this->emit('error', 'You are already on the waitlist. <br>Please check your email to verify your email address.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->emit('error', 'You are already on the waitlist.');
|
$this->emit('error', 'You are already on the waitlist. <br>You will be notified when your turn comes. <br>Thank you.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$waitlist = ModelsWaitlist::create([
|
$waitlist = ModelsWaitlist::create([
|
||||||
@ -44,11 +43,10 @@ class Waitlist extends Component
|
|||||||
'type' => 'registration',
|
'type' => 'registration',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->emit('success', 'You have been added to the waitlist.');
|
$this->emit('success', 'Check your email to verify your email address.');
|
||||||
dispatch(new SendConfirmationForWaitlistJob($this->email, $waitlist->uuid));
|
dispatch(new SendConfirmationForWaitlistJob($this->email, $waitlist->uuid));
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return general_error_handler(err: $e, that: $this);
|
return general_error_handler(err: $e, that: $this);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique
|
|||||||
try {
|
try {
|
||||||
$this->cleanup_waitlist();
|
$this->cleanup_waitlist();
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
send_internal_notification('CleanupInstanceStuffsJob failed with error: ' . $e->getMessage());
|
||||||
ray($e->getMessage());
|
ray($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeUnique
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$status = get_container_status(server: $this->resource->destination->server, container_id: $this->container_name, throwError: false);
|
$status = get_container_status(server: $this->resource->destination->server, container_id: $this->container_name, throwError: false);
|
||||||
if ($this->resource->status === 'running' && $status === 'stopped') {
|
if ($this->resource->status === 'running' && $status !== 'running') {
|
||||||
$this->resource->environment->project->team->notify(new StatusChanged($this->resource));
|
$this->resource->environment->project->team->notify(new StatusChanged($this->resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ use Illuminate\Queue\InteractsWithQueue;
|
|||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class DatabaseBackupJob implements ShouldQueue
|
class DatabaseBackupJob implements ShouldQueue
|
||||||
{
|
{
|
||||||
@ -68,11 +69,13 @@ class DatabaseBackupJob implements ShouldQueue
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->container_name = $this->database->uuid;
|
$this->container_name = $this->database->uuid;
|
||||||
|
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name;
|
||||||
|
|
||||||
if ($this->database->name === 'coolify-db') {
|
if ($this->database->name === 'coolify-db') {
|
||||||
$this->container_name = "coolify-db";
|
$this->container_name = "coolify-db";
|
||||||
|
$ip = Str::slug($this->server->ip);
|
||||||
|
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->backup_dir = backup_dir() . "/" . $this->container_name;
|
|
||||||
$this->backup_file = "/dumpall-" . Carbon::now()->timestamp . ".sql";
|
$this->backup_file = "/dumpall-" . Carbon::now()->timestamp . ".sql";
|
||||||
$this->backup_location = $this->backup_dir . $this->backup_file;
|
$this->backup_location = $this->backup_dir . $this->backup_file;
|
||||||
|
|
||||||
@ -95,6 +98,7 @@ class DatabaseBackupJob implements ShouldQueue
|
|||||||
private function backup_standalone_postgresql(): void
|
private function backup_standalone_postgresql(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
ray($this->backup_dir);
|
||||||
$commands[] = "mkdir -p " . $this->backup_dir;
|
$commands[] = "mkdir -p " . $this->backup_dir;
|
||||||
$commands[] = "docker exec $this->container_name pg_dumpall -U {$this->database->postgres_user} > $this->backup_location";
|
$commands[] = "docker exec $this->container_name pg_dumpall -U {$this->database->postgres_user} > $this->backup_location";
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
|||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
class InstanceApplicationsStatusJob implements ShouldQueue, ShouldBeUnique
|
class ResourceStatusJob implements ShouldQueue, ShouldBeUnique
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
@ -24,10 +24,6 @@ class SendConfirmationForWaitlistJob implements ShouldQueue
|
|||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$settings = InstanceSettings::get();
|
|
||||||
|
|
||||||
|
|
||||||
set_transanctional_email_settings($settings);
|
|
||||||
$mail = new MailMessage();
|
$mail = new MailMessage();
|
||||||
|
|
||||||
$confirmation_url = base_url() . '/webhooks/waitlist/confirm?email=' . $this->email . '&confirmation_code=' . $this->uuid;
|
$confirmation_url = base_url() . '/webhooks/waitlist/confirm?email=' . $this->email . '&confirmation_code=' . $this->uuid;
|
||||||
@ -39,19 +35,9 @@ class SendConfirmationForWaitlistJob implements ShouldQueue
|
|||||||
'cancel_url' => $cancel_url,
|
'cancel_url' => $cancel_url,
|
||||||
]);
|
]);
|
||||||
$mail->subject('You are on the waitlist!');
|
$mail->subject('You are on the waitlist!');
|
||||||
Mail::send(
|
send_user_an_email($mail, $this->email);
|
||||||
[],
|
|
||||||
[],
|
|
||||||
fn(Message $message) => $message
|
|
||||||
->from(
|
|
||||||
data_get($settings, 'smtp_from_address'),
|
|
||||||
data_get($settings, 'smtp_from_name')
|
|
||||||
)
|
|
||||||
->to($this->email)
|
|
||||||
->subject($mail->subject)
|
|
||||||
->html((string) $mail->render())
|
|
||||||
);
|
|
||||||
} catch (\Throwable $th) {
|
} catch (\Throwable $th) {
|
||||||
|
send_internal_notification('SendConfirmationForWaitlistJob failed with error: ' . $th->getMessage());
|
||||||
ray($th->getMessage());
|
ray($th->getMessage());
|
||||||
throw $th;
|
throw $th;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ 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 App\Notifications\TrnsactionalEmails\ResetPassword;
|
|
||||||
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;
|
||||||
|
27
app/Notifications/Internal/GeneralNotification.php
Normal file
27
app/Notifications/Internal/GeneralNotification.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications\Internal;
|
||||||
|
|
||||||
|
use App\Notifications\Channels\DiscordChannel;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
|
class GeneralNotification extends Notification implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public function __construct(public string $message)
|
||||||
|
{}
|
||||||
|
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
$channels[] = DiscordChannel::class;
|
||||||
|
return $channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toDiscord(): string
|
||||||
|
{
|
||||||
|
return $this->message;
|
||||||
|
}
|
||||||
|
}
|
58
app/Notifications/Server/NotReachable.php
Normal file
58
app/Notifications/Server/NotReachable.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?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 function __construct(public Server $server)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
$channels = [];
|
||||||
|
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
|
||||||
|
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
|
||||||
|
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_status_changes');
|
||||||
|
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_status_changes');
|
||||||
|
|
||||||
|
// if ($isEmailEnabled && $isSubscribedToEmailEvent) {
|
||||||
|
// $channels[] = EmailChannel::class;
|
||||||
|
// }
|
||||||
|
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
|
||||||
|
$channels[] = DiscordChannel::class;
|
||||||
|
}
|
||||||
|
return $channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ use App\Actions\Fortify\UpdateUserPassword;
|
|||||||
use App\Actions\Fortify\UpdateUserProfileInformation;
|
use App\Actions\Fortify\UpdateUserProfileInformation;
|
||||||
use App\Models\InstanceSettings;
|
use App\Models\InstanceSettings;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\Waitlist;
|
||||||
use Illuminate\Cache\RateLimiting\Limit;
|
use Illuminate\Cache\RateLimiting\Limit;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
@ -45,15 +46,19 @@ class FortifyServiceProvider extends ServiceProvider
|
|||||||
|
|
||||||
Fortify::createUsersUsing(CreateNewUser::class);
|
Fortify::createUsersUsing(CreateNewUser::class);
|
||||||
Fortify::registerView(function () {
|
Fortify::registerView(function () {
|
||||||
ray('asd');
|
|
||||||
$settings = InstanceSettings::get();
|
$settings = InstanceSettings::get();
|
||||||
|
$waiting_in_line = Waitlist::whereVerified(true)->count();
|
||||||
if (!$settings->is_registration_enabled) {
|
if (!$settings->is_registration_enabled) {
|
||||||
return redirect()->route('login');
|
return redirect()->route('login');
|
||||||
}
|
}
|
||||||
if (config('coolify.waitlist')) {
|
if (config('coolify.waitlist')) {
|
||||||
return view('auth.waitlist');
|
return view('auth.waitlist',[
|
||||||
|
'waiting_in_line' => $waiting_in_line,
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
return view('auth.register');
|
return view('auth.register',[
|
||||||
|
'waiting_in_line' => $waiting_in_line,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -75,6 +80,8 @@ class FortifyServiceProvider extends ServiceProvider
|
|||||||
$user &&
|
$user &&
|
||||||
Hash::check($request->password, $user->password)
|
Hash::check($request->password, $user->password)
|
||||||
) {
|
) {
|
||||||
|
$user->updated_at = now();
|
||||||
|
$user->save();
|
||||||
session(['currentTeam' => $user->currentTeam = $user->teams->firstWhere('personal_team', true)]);
|
session(['currentTeam' => $user->currentTeam = $user->teams->firstWhere('personal_team', true)]);
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
function format_docker_command_output_to_json($rawOutput): Collection
|
function format_docker_command_output_to_json($rawOutput): Collection
|
||||||
{
|
{
|
||||||
@ -45,6 +46,7 @@ function format_docker_envs_to_json($rawOutput)
|
|||||||
|
|
||||||
function get_container_status(Server $server, string $container_id, bool $all_data = false, bool $throwError = false)
|
function get_container_status(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';
|
||||||
@ -53,7 +55,7 @@ function get_container_status(Server $server, string $container_id, bool $all_da
|
|||||||
if ($all_data) {
|
if ($all_data) {
|
||||||
return $container[0];
|
return $container[0];
|
||||||
}
|
}
|
||||||
return $container[0]['State']['Status'];
|
return data_get($container[0], 'State.Status', 'exited');
|
||||||
}
|
}
|
||||||
|
|
||||||
function generate_container_name(string $uuid, int $pull_request_id = 0)
|
function generate_container_name(string $uuid, int $pull_request_id = 0)
|
||||||
@ -66,11 +68,17 @@ function generate_container_name(string $uuid, int $pull_request_id = 0)
|
|||||||
}
|
}
|
||||||
function get_port_from_dockerfile($dockerfile): int
|
function get_port_from_dockerfile($dockerfile): int
|
||||||
{
|
{
|
||||||
$port = preg_grep('/EXPOSE\s+(\d+)/', explode("\n", $dockerfile));
|
$dockerfile_array = explode("\n", $dockerfile);
|
||||||
if (count($port) > 0 && preg_match('/EXPOSE\s+(\d+)/', $port[1], $matches)) {
|
$found_exposed_port = null;
|
||||||
$port = $matches[1];
|
foreach ($dockerfile_array as $line) {
|
||||||
} else {
|
$line_str = Str::of($line)->trim();
|
||||||
$port = 80;
|
if ($line_str->startsWith('EXPOSE')) {
|
||||||
|
$found_exposed_port = $line_str->replace('EXPOSE', '')->trim();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return $port;
|
if ($found_exposed_port) {
|
||||||
|
return (int)$found_exposed_port->value();
|
||||||
|
}
|
||||||
|
return 80;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ use App\Models\Application;
|
|||||||
use App\Models\ApplicationDeploymentQueue;
|
use App\Models\ApplicationDeploymentQueue;
|
||||||
use App\Models\PrivateKey;
|
use App\Models\PrivateKey;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Notifications\Server\NotReachable;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
@ -109,8 +110,8 @@ function instant_remote_process(array $command, Server $server, $throwError = tr
|
|||||||
$exitCode = $process->exitCode();
|
$exitCode = $process->exitCode();
|
||||||
if ($exitCode !== 0) {
|
if ($exitCode !== 0) {
|
||||||
if ($repeat > 1) {
|
if ($repeat > 1) {
|
||||||
|
ray("repeat: ", $repeat);
|
||||||
Sleep::for(200)->milliseconds();
|
Sleep::for(200)->milliseconds();
|
||||||
ray('executing again');
|
|
||||||
return instant_remote_process($command, $server, $throwError, $repeat - 1);
|
return instant_remote_process($command, $server, $throwError, $repeat - 1);
|
||||||
}
|
}
|
||||||
// ray('ERROR OCCURED: ' . $process->errorOutput());
|
// ray('ERROR OCCURED: ' . $process->errorOutput());
|
||||||
@ -152,12 +153,13 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
|
|||||||
return $formatted;
|
return $formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshPrivateKey(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
|
// 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());
|
||||||
if (auth()->user()->currentTeam()->id) {
|
// check if user is authenticated
|
||||||
|
if (auth()?->user()?->currentTeam()->id) {
|
||||||
auth()->user()->currentTeam()->privateKeys = PrivateKey::where('team_id', auth()->user()->currentTeam()->id)->get();
|
auth()->user()->currentTeam()->privateKeys = PrivateKey::where('team_id', auth()->user()->currentTeam()->id)->get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -166,7 +168,7 @@ function refreshPrivateKey(PrivateKey $private_key)
|
|||||||
function validateServer(Server $server)
|
function validateServer(Server $server)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
refreshPrivateKey($server->privateKey);
|
refresh_server_connection($server->privateKey);
|
||||||
$uptime = instant_remote_process(['uptime'], $server);
|
$uptime = instant_remote_process(['uptime'], $server);
|
||||||
if (!$uptime) {
|
if (!$uptime) {
|
||||||
$uptime = 'Server not reachable.';
|
$uptime = 'Server not reachable.';
|
||||||
@ -192,3 +194,26 @@ function validateServer(Server $server)
|
|||||||
$server->settings->save();
|
$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 (\Exception $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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Models\InstanceSettings;
|
use App\Models\InstanceSettings;
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Notifications\Internal\GeneralNotification;
|
||||||
|
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
|
||||||
use Illuminate\Database\QueryException;
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Mail\Message;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Nubs\RandomNameGenerator\All;
|
use Nubs\RandomNameGenerator\All;
|
||||||
use Poliander\Cron\CronExpression;
|
use Poliander\Cron\CronExpression;
|
||||||
use Visus\Cuid2\Cuid2;
|
use Visus\Cuid2\Cuid2;
|
||||||
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
|
|
||||||
|
|
||||||
function application_configuration_dir(): string
|
function application_configuration_dir(): string
|
||||||
{
|
{
|
||||||
@ -35,7 +40,7 @@ function is_instance_admin()
|
|||||||
return auth()->user()?->isInstanceAdmin();
|
return auth()->user()?->isInstanceAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
function general_error_handler(Throwable|null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed
|
function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
ray('ERROR OCCURRED: ' . $err->getMessage());
|
ray('ERROR OCCURRED: ' . $err->getMessage());
|
||||||
@ -47,9 +52,9 @@ function general_error_handler(Throwable|null $err = null, $that = null, $isJson
|
|||||||
} else {
|
} else {
|
||||||
throw new Exception($customErrorMessage ?? $err->errorInfo[2]);
|
throw new Exception($customErrorMessage ?? $err->errorInfo[2]);
|
||||||
}
|
}
|
||||||
} elseif($err instanceof TooManyRequestsException){
|
} elseif ($err instanceof TooManyRequestsException) {
|
||||||
throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds.");
|
throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds.");
|
||||||
}else {
|
} else {
|
||||||
throw new Exception($customErrorMessage ?? $err->getMessage());
|
throw new Exception($customErrorMessage ?? $err->getMessage());
|
||||||
}
|
}
|
||||||
} catch (Throwable $error) {
|
} catch (Throwable $error) {
|
||||||
@ -104,7 +109,7 @@ function is_transactional_emails_active(): bool
|
|||||||
return data_get(InstanceSettings::get(), 'smtp_enabled');
|
return data_get(InstanceSettings::get(), 'smtp_enabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
function set_transanctional_email_settings(InstanceSettings|null $settings = null): void
|
function set_transanctional_email_settings(InstanceSettings | null $settings = null): void
|
||||||
{
|
{
|
||||||
if (!$settings) {
|
if (!$settings) {
|
||||||
$settings = InstanceSettings::get();
|
$settings = InstanceSettings::get();
|
||||||
@ -130,10 +135,16 @@ function set_transanctional_email_settings(InstanceSettings|null $settings = nul
|
|||||||
function base_ip(): string
|
function base_ip(): string
|
||||||
{
|
{
|
||||||
if (is_dev()) {
|
if (is_dev()) {
|
||||||
return "http://localhost";
|
return "localhost";
|
||||||
}
|
}
|
||||||
$settings = InstanceSettings::get();
|
$settings = InstanceSettings::get();
|
||||||
return "http://$settings->public_ipv4";
|
if ($settings->public_ipv4) {
|
||||||
|
return "$settings->public_ipv4";
|
||||||
|
}
|
||||||
|
if ($settings->public_ipv6) {
|
||||||
|
return "$settings->public_ipv6";
|
||||||
|
}
|
||||||
|
return "localhost";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -188,3 +199,29 @@ function validate_cron_expression($expression_to_validate): bool
|
|||||||
}
|
}
|
||||||
return $isValid;
|
return $isValid;
|
||||||
}
|
}
|
||||||
|
function send_internal_notification(string $message): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$team = Team::find(0);
|
||||||
|
$team->notify(new GeneralNotification('👀 Internal notifications: ' . $message));
|
||||||
|
} catch (\Throwable $th) {
|
||||||
|
ray($th->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function send_user_an_email(MailMessage $mail, string $email): void
|
||||||
|
{
|
||||||
|
$settings = InstanceSettings::get();
|
||||||
|
set_transanctional_email_settings($settings);
|
||||||
|
Mail::send(
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
fn (Message $message) => $message
|
||||||
|
->from(
|
||||||
|
data_get($settings, 'smtp_from_address'),
|
||||||
|
data_get($settings, 'smtp_from_name')
|
||||||
|
)
|
||||||
|
->to($email)
|
||||||
|
->subject($mail->subject)
|
||||||
|
->html((string) $mail->render())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@ return [
|
|||||||
'self_hosted' => env('SELF_HOSTED', true),
|
'self_hosted' => env('SELF_HOSTED', true),
|
||||||
'waitlist' => env('WAITLIST', false),
|
'waitlist' => env('WAITLIST', false),
|
||||||
'license_url' => 'https://license.coolify.io',
|
'license_url' => 'https://license.coolify.io',
|
||||||
|
'lemon_squeezy_api_key' => env('LEMON_SQUEEZY_API_KEY', null),
|
||||||
'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', null),
|
'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', null),
|
||||||
'lemon_squeezy_checkout_id_monthly_basic' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_BASIC', null),
|
'lemon_squeezy_checkout_id_monthly_basic' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_BASIC', null),
|
||||||
'lemon_squeezy_checkout_id_monthly_pro' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_PRO', null),
|
'lemon_squeezy_checkout_id_monthly_pro' => env('LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_PRO', null),
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return '4.0.0-beta.19';
|
return '4.0.0-beta.20';
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
<?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('servers', function (Blueprint $table) {
|
||||||
|
$table->integer('unreachable_count')->default(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('servers', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('unreachable_count');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -33,6 +33,7 @@ services:
|
|||||||
- PHP_PM_MIN_SPARE_SERVERS=1
|
- PHP_PM_MIN_SPARE_SERVERS=1
|
||||||
- PHP_PM_MAX_SPARE_SERVERS=10
|
- PHP_PM_MAX_SPARE_SERVERS=10
|
||||||
- SELF_HOSTED
|
- SELF_HOSTED
|
||||||
|
- WAITLIST
|
||||||
- LEMON_SQUEEZY_WEBHOOK_SECRET
|
- LEMON_SQUEEZY_WEBHOOK_SECRET
|
||||||
- LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_BASIC
|
- LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_BASIC
|
||||||
- LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_PRO
|
- LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_PRO
|
||||||
|
BIN
public/coolify-transparent.png
Normal file
BIN
public/coolify-transparent.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
BIN
public/coolify.png
Normal file
BIN
public/coolify.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
@ -128,6 +128,15 @@
|
|||||||
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
|
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
|
||||||
<path d="M10 16l0 .01" />
|
<path d="M10 16l0 .01" />
|
||||||
</template>
|
</template>
|
||||||
|
<template
|
||||||
|
v-if="action.icon === 'storage' || sequenceState.sequence[sequenceState.currentActionIndex] === 'storage'">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" stroke-width="2">
|
||||||
|
<path d="M4 6a8 3 0 1 0 16 0A8 3 0 1 0 4 6" />
|
||||||
|
<path d="M4 6v6a8 3 0 0 0 16 0V6" />
|
||||||
|
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
||||||
|
</g>
|
||||||
|
</template>
|
||||||
<template
|
<template
|
||||||
v-if="action.icon === 'project' || sequenceState.sequence[sequenceState.currentActionIndex] === 'project'">
|
v-if="action.icon === 'project' || sequenceState.sequence[sequenceState.currentActionIndex] === 'project'">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
@ -273,6 +282,14 @@ const magicActions = [{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
|
name: 'Deploy: Dockerfile',
|
||||||
|
tags: 'dockerfile,deploy',
|
||||||
|
icon: 'destination',
|
||||||
|
new: true,
|
||||||
|
sequence: ['main', 'server', 'destination', 'project', 'environment', 'redirect']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
name: 'Create: Server',
|
name: 'Create: Server',
|
||||||
tags: 'server,ssh,new,create',
|
tags: 'server,ssh,new,create',
|
||||||
icon: 'server',
|
icon: 'server',
|
||||||
@ -280,7 +297,7 @@ const magicActions = [{
|
|||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 6,
|
||||||
name: 'Create: Source',
|
name: 'Create: Source',
|
||||||
tags: 'source,git,gitlab,github,bitbucket,gitea,new,create',
|
tags: 'source,git,gitlab,github,bitbucket,gitea,new,create',
|
||||||
icon: 'git',
|
icon: 'git',
|
||||||
@ -288,7 +305,7 @@ const magicActions = [{
|
|||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 7,
|
||||||
name: 'Create: Private Key',
|
name: 'Create: Private Key',
|
||||||
tags: 'private,key,ssh,new,create',
|
tags: 'private,key,ssh,new,create',
|
||||||
icon: 'key',
|
icon: 'key',
|
||||||
@ -296,16 +313,15 @@ const magicActions = [{
|
|||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 8,
|
||||||
name: 'Create: Destination',
|
name: 'Create: Destination',
|
||||||
tags: 'destination,docker,network,new,create',
|
tags: 'destination,docker,network,new,create',
|
||||||
icon: 'destination',
|
icon: 'destination',
|
||||||
new: true,
|
new: true,
|
||||||
sequence: ['main', 'server', 'redirect']
|
sequence: ['main', 'server', 'redirect']
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 9,
|
||||||
name: 'Create: Team',
|
name: 'Create: Team',
|
||||||
tags: 'team,member,new,create',
|
tags: 'team,member,new,create',
|
||||||
icon: 'team',
|
icon: 'team',
|
||||||
@ -313,74 +329,82 @@ const magicActions = [{
|
|||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 10,
|
||||||
|
name: 'Create: S3 Storage',
|
||||||
|
tags: 's3,storage,new,create',
|
||||||
|
icon: 'storage',
|
||||||
|
new: true,
|
||||||
|
sequence: ['main', 'redirect']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
name: 'Goto: Dashboard',
|
name: 'Goto: Dashboard',
|
||||||
icon: 'goto',
|
icon: 'goto',
|
||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10,
|
id: 12,
|
||||||
name: 'Goto: Servers',
|
name: 'Goto: Servers',
|
||||||
icon: 'goto',
|
icon: 'goto',
|
||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 11,
|
id: 13,
|
||||||
name: 'Goto: Private Keys',
|
name: 'Goto: Private Keys',
|
||||||
tags: 'destination,docker,network,new,create,ssh,private,key',
|
tags: 'destination,docker,network,new,create,ssh,private,key',
|
||||||
icon: 'goto',
|
icon: 'goto',
|
||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 12,
|
id: 14,
|
||||||
name: 'Goto: Projects',
|
name: 'Goto: Projects',
|
||||||
icon: 'goto',
|
icon: 'goto',
|
||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 13,
|
id: 15,
|
||||||
name: 'Goto: Sources',
|
name: 'Goto: Sources',
|
||||||
icon: 'goto',
|
icon: 'goto',
|
||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 14,
|
id: 16,
|
||||||
name: 'Goto: Destinations',
|
name: 'Goto: Destinations',
|
||||||
icon: 'goto',
|
icon: 'goto',
|
||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 15,
|
id: 17,
|
||||||
name: 'Goto: Settings',
|
name: 'Goto: Settings',
|
||||||
icon: 'goto',
|
icon: 'goto',
|
||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 16,
|
id: 18,
|
||||||
name: 'Goto: Command Center',
|
name: 'Goto: Command Center',
|
||||||
icon: 'goto',
|
icon: 'goto',
|
||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 17,
|
id: 19,
|
||||||
name: 'Goto: Notifications',
|
name: 'Goto: Notifications',
|
||||||
icon: 'goto',
|
icon: 'goto',
|
||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 18,
|
id: 20,
|
||||||
name: 'Goto: Profile',
|
name: 'Goto: Profile',
|
||||||
icon: 'goto',
|
icon: 'goto',
|
||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 19,
|
id: 21,
|
||||||
name: 'Goto: Teams',
|
name: 'Goto: Teams',
|
||||||
icon: 'goto',
|
icon: 'goto',
|
||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 20,
|
id: 22,
|
||||||
name: 'Goto: Switch Teams',
|
name: 'Goto: Switch Teams',
|
||||||
icon: 'goto',
|
icon: 'goto',
|
||||||
sequence: ['main', 'redirect']
|
sequence: ['main', 'redirect']
|
||||||
@ -552,55 +576,63 @@ async function redirect() {
|
|||||||
targetUrl.searchParams.append('destination', destination)
|
targetUrl.searchParams.append('destination', destination)
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
targetUrl.pathname = `/server/new`
|
targetUrl.pathname = `/project/${project}/${environment}/new`
|
||||||
|
targetUrl.searchParams.append('type', 'dockerfile')
|
||||||
|
targetUrl.searchParams.append('destination', destination)
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
targetUrl.pathname = `/source/new`
|
targetUrl.pathname = `/server/new`
|
||||||
break;
|
break;
|
||||||
case 6:
|
case 6:
|
||||||
targetUrl.pathname = `/private-key/new`
|
targetUrl.pathname = `/source/new`
|
||||||
break;
|
break;
|
||||||
case 7:
|
case 7:
|
||||||
|
targetUrl.pathname = `/private-key/new`
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
targetUrl.pathname = `/destination/new`
|
targetUrl.pathname = `/destination/new`
|
||||||
targetUrl.searchParams.append('server', server)
|
targetUrl.searchParams.append('server', server)
|
||||||
break;
|
break;
|
||||||
case 8:
|
case 9:
|
||||||
targetUrl.pathname = `/team/new`
|
targetUrl.pathname = `/team/new`
|
||||||
break;
|
break;
|
||||||
case 9:
|
|
||||||
targetUrl.pathname = `/`
|
|
||||||
break;
|
|
||||||
case 10:
|
case 10:
|
||||||
targetUrl.pathname = `/servers`
|
targetUrl.pathname = `/team/storages/new`
|
||||||
break;
|
break;
|
||||||
case 11:
|
case 11:
|
||||||
targetUrl.pathname = `/private-keys`
|
targetUrl.pathname = `/`
|
||||||
break;
|
break;
|
||||||
case 12:
|
case 12:
|
||||||
targetUrl.pathname = `/projects`
|
targetUrl.pathname = `/servers`
|
||||||
break;
|
break;
|
||||||
case 13:
|
case 13:
|
||||||
targetUrl.pathname = `/sources`
|
targetUrl.pathname = `/private-keys`
|
||||||
break;
|
break;
|
||||||
case 14:
|
case 14:
|
||||||
targetUrl.pathname = `/destinations`
|
targetUrl.pathname = `/projects`
|
||||||
break;
|
break;
|
||||||
case 15:
|
case 15:
|
||||||
targetUrl.pathname = `/settings`
|
targetUrl.pathname = `/sources`
|
||||||
break;
|
break;
|
||||||
case 16:
|
case 16:
|
||||||
targetUrl.pathname = `/command-center`
|
targetUrl.pathname = `/destinations`
|
||||||
break;
|
break;
|
||||||
case 17:
|
case 17:
|
||||||
targetUrl.pathname = `/team/notifications`
|
targetUrl.pathname = `/settings`
|
||||||
break;
|
break;
|
||||||
case 18:
|
case 18:
|
||||||
targetUrl.pathname = `/profile`
|
targetUrl.pathname = `/command-center`
|
||||||
break;
|
break;
|
||||||
case 19:
|
case 19:
|
||||||
targetUrl.pathname = `/team`
|
targetUrl.pathname = `/team/notifications`
|
||||||
break;
|
break;
|
||||||
case 20:
|
case 20:
|
||||||
|
targetUrl.pathname = `/profile`
|
||||||
|
break;
|
||||||
|
case 21:
|
||||||
|
targetUrl.pathname = `/team`
|
||||||
|
break;
|
||||||
|
case 22:
|
||||||
targetUrl.pathname = `/team`
|
targetUrl.pathname = `/team`
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,10 @@
|
|||||||
<link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet">
|
<link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet">
|
||||||
@env('local')
|
@env('local')
|
||||||
<title>Coolify - localhost</title>
|
<title>Coolify - localhost</title>
|
||||||
@endenv
|
<link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" />
|
||||||
@env('production')
|
@else
|
||||||
<title>{{ $title ?? 'Coolify' }}</title>
|
<title>{{ $title ?? 'Coolify' }}</title>
|
||||||
|
<link rel="icon" href="{{ asset('coolify-transparent.png') }}" type="image/x-icon" />
|
||||||
@endenv
|
@endenv
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
@vite(['resources/js/app.js', 'resources/css/app.css'])
|
@vite(['resources/js/app.js', 'resources/css/app.css'])
|
||||||
@ -25,7 +26,7 @@
|
|||||||
<body>
|
<body>
|
||||||
@livewireScripts
|
@livewireScripts
|
||||||
<x-toaster-hub />
|
<x-toaster-hub />
|
||||||
<main>
|
<main class="main max-w-screen-2xl">
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
</main>
|
</main>
|
||||||
<x-version class="fixed left-2 bottom-1" />
|
<x-version class="fixed left-2 bottom-1" />
|
||||||
|
@ -8,9 +8,10 @@
|
|||||||
<link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet">
|
<link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet">
|
||||||
@env('local')
|
@env('local')
|
||||||
<title>Coolify - localhost</title>
|
<title>Coolify - localhost</title>
|
||||||
@endenv
|
<link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" />
|
||||||
@env('production')
|
@else
|
||||||
<title>{{ $title ?? 'Coolify' }}</title>
|
<title>{{ $title ?? 'Coolify' }}</title>
|
||||||
|
<link rel="icon" href="{{ asset('coolify-transparent.png') }}" type="image/x-icon" />
|
||||||
@endenv
|
@endenv
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
@vite(['resources/js/app.js', 'resources/css/app.css'])
|
@vite(['resources/js/app.js', 'resources/css/app.css'])
|
||||||
@ -57,8 +58,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Livewire.on('reloadWindow', () => {
|
Livewire.on('reloadWindow', (timeout) => {
|
||||||
window.location.reload();
|
if (timeout) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, timeout);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
@ -9,10 +9,9 @@
|
|||||||
@env('local')
|
@env('local')
|
||||||
<title>Coolify - localhost</title>
|
<title>Coolify - localhost</title>
|
||||||
<link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" />
|
<link rel="icon" href="{{ asset('favicon-dev.png') }}" type="image/x-icon" />
|
||||||
@endenv
|
@else
|
||||||
@env('production')
|
|
||||||
<link rel="icon" href="{{ asset('favicon.png') }}" type="image/x-icon" />
|
|
||||||
<title>{{ $title ?? 'Coolify' }}</title>
|
<title>{{ $title ?? 'Coolify' }}</title>
|
||||||
|
<link rel="icon" href="{{ asset('coolify-transparent.png') }}" type="image/x-icon" />
|
||||||
@endenv
|
@endenv
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
@vite(['resources/js/app.js', 'resources/css/app.css'])
|
@vite(['resources/js/app.js', 'resources/css/app.css'])
|
||||||
@ -102,8 +101,15 @@
|
|||||||
Livewire.emit('message', 'Copied to clipboard.');
|
Livewire.emit('message', 'Copied to clipboard.');
|
||||||
}
|
}
|
||||||
|
|
||||||
Livewire.on('reloadWindow', () => {
|
Livewire.on('reloadWindow', (timeout) => {
|
||||||
window.location.reload();
|
if (timeout) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, timeout);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
Livewire.on('info', (message) => {
|
Livewire.on('info', (message) => {
|
||||||
if (message) Toaster.info(message)
|
if (message) Toaster.info(message)
|
||||||
|
@ -50,6 +50,20 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li title="Teams">
|
||||||
|
<a class="hover:bg-transparent" href="{{ route('team.show') }}">
|
||||||
|
<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" />
|
||||||
|
<path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||||
|
<path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1" />
|
||||||
|
<path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||||
|
<path d="M17 10h2a2 2 0 0 1 2 2v1" />
|
||||||
|
<path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||||
|
<path d="M3 13v-1a2 2 0 0 1 2 -2h2" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
@if (is_instance_admin())
|
@if (is_instance_admin())
|
||||||
<livewire:upgrade />
|
<livewire:upgrade />
|
||||||
@ -65,20 +79,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li title="Teams">
|
|
||||||
<a class="hover:bg-transparent" href="{{ route('team.show') }}">
|
|
||||||
<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" />
|
|
||||||
<path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
|
||||||
<path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1" />
|
|
||||||
<path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
|
||||||
<path d="M17 10h2a2 2 0 0 1 2 2v1" />
|
|
||||||
<path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
|
||||||
<path d="M3 13v-1a2 2 0 0 1 2 -2h2" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
@if (is_instance_admin())
|
@if (is_instance_admin())
|
||||||
<li title="Settings" class="mt-auto">
|
<li title="Settings" class="mt-auto">
|
||||||
<a class="hover:bg-transparent" @if (!request()->is('settings')) href="/settings" @endif>
|
<a class="hover:bg-transparent" @if (!request()->is('settings')) href="/settings" @endif>
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
<div x-data="{ selected: 'monthly' }" class="w-full">
|
@props([
|
||||||
|
'showSubscribeButtons' => true,
|
||||||
|
])
|
||||||
|
<div x-data="{ selected: 'yearly' }" class="w-full pb-20">
|
||||||
<div class="px-6 mx-auto lg:px-8">
|
<div class="px-6 mx-auto lg:px-8">
|
||||||
<div class="flex justify-center mt-5">
|
<div class="flex justify-center mt-5">
|
||||||
<fieldset
|
<fieldset
|
||||||
@ -14,7 +17,7 @@
|
|||||||
:class="selected === 'yearly' ? 'bg-coollabs-100 text-white' : ''">
|
:class="selected === 'yearly' ? 'bg-coollabs-100 text-white' : ''">
|
||||||
<input type="radio" x-on:click="selected = 'yearly'" name="frequency" value="annually"
|
<input type="radio" x-on:click="selected = 'yearly'" name="frequency" value="annually"
|
||||||
class="sr-only">
|
class="sr-only">
|
||||||
<span>Annually</span>
|
<span>Annually <span class="text-xs text-warning">(save ~1 month)<span></span>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
@ -39,7 +42,12 @@
|
|||||||
<span class="text-4xl font-bold tracking-tight text-white">Still Free </span>
|
<span class="text-4xl font-bold tracking-tight text-white">Still Free </span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<span x-show="selected === 'yearly'" x-cloak class="text-warning">(save $?)</span>
|
<span x-show="selected === 'monthly'" x-cloak>
|
||||||
|
<span>billed monthly</span>
|
||||||
|
</span>
|
||||||
|
<span x-show="selected === 'yearly'" x-cloak>
|
||||||
|
<span>billed annually</span>
|
||||||
|
</span>
|
||||||
<a href="https://github.com/coollabsio/coolify" aria-describedby="tier-trial" class="buyme">Get
|
<a href="https://github.com/coollabsio/coolify" aria-describedby="tier-trial" class="buyme">Get
|
||||||
Started</a>
|
Started</a>
|
||||||
<p class="mt-10 text-sm leading-6 text-white h-[6.5rem]">Start self-hosting without limits with our
|
<p class="mt-10 text-sm leading-6 text-white h-[6.5rem]">Start self-hosting without limits with our
|
||||||
@ -53,25 +61,7 @@
|
|||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||||
clip-rule="evenodd" />
|
clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
Same features as the paid version
|
You manage everything
|
||||||
</li>
|
|
||||||
<li class="flex gap-x-3">
|
|
||||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
|
||||||
aria-hidden="true">
|
|
||||||
<path fill-rule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
||||||
clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Managed by you
|
|
||||||
</li>
|
|
||||||
<li class="flex gap-x-3">
|
|
||||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
|
||||||
aria-hidden="true">
|
|
||||||
<path fill-rule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
||||||
clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
If you brave enough
|
|
||||||
</li>
|
</li>
|
||||||
<li class="flex gap-x-3">
|
<li class="flex gap-x-3">
|
||||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||||
@ -101,19 +91,25 @@
|
|||||||
<p class="flex items-baseline mt-6 gap-x-1">
|
<p class="flex items-baseline mt-6 gap-x-1">
|
||||||
<span x-show="selected === 'monthly'" x-cloak>
|
<span x-show="selected === 'monthly'" x-cloak>
|
||||||
<span class="text-4xl font-bold tracking-tight text-white">$5</span>
|
<span class="text-4xl font-bold tracking-tight text-white">$5</span>
|
||||||
<span class="text-sm font-semibold leading-6 ">/monthly</span>
|
<span class="text-sm font-semibold leading-6 ">/month</span>
|
||||||
</span>
|
</span>
|
||||||
<span x-show="selected === 'yearly'" x-cloak>
|
<span x-show="selected === 'yearly'" x-cloak>
|
||||||
<span class="text-4xl font-bold tracking-tight text-white">$54</span>
|
<span class="text-4xl font-bold tracking-tight text-white">$4</span>
|
||||||
<span class="text-sm font-semibold leading-6 ">/yearly</span>
|
<span class="text-sm font-semibold leading-6 ">/month</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<span x-show="selected === 'yearly'" x-cloak class="text-warning">(save $6)</span>
|
<span x-show="selected === 'monthly'" x-cloak>
|
||||||
|
<span>billed monthly</span>
|
||||||
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic" class="buyme"
|
</span>
|
||||||
href="{{ getSubscriptionLink('monthly_basic') }}">Subscribe</a>
|
<span x-show="selected === 'yearly'" x-cloak>
|
||||||
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic" class="buyme"
|
<span>billed annually</span>
|
||||||
href="{{ getSubscriptionLink('yearly') }}">Subscribe</a>
|
</span>
|
||||||
|
@if ($showSubscribeButtons)
|
||||||
|
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-basic" class="buyme"
|
||||||
|
href="{{ getSubscriptionLink('monthly_basic') }}">Subscribe</a>
|
||||||
|
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-basic" class="buyme"
|
||||||
|
href="{{ getSubscriptionLink('yearly_basic') }}">Subscribe</a>
|
||||||
|
@endif
|
||||||
<p class="mt-10 text-sm leading-6 text-white h-[6.5rem]">Start self-hosting in
|
<p class="mt-10 text-sm leading-6 text-white h-[6.5rem]">Start self-hosting in
|
||||||
the cloud
|
the cloud
|
||||||
with a
|
with a
|
||||||
@ -128,25 +124,7 @@
|
|||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||||
clip-rule="evenodd" />
|
clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
1 server
|
1 server <x-helper helper="Bring Your Own Server. All you need is n SSH connection." />
|
||||||
</li>
|
|
||||||
<li class="flex gap-x-3">
|
|
||||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
|
||||||
aria-hidden="true">
|
|
||||||
<path fill-rule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
||||||
clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Unlimited Deployments
|
|
||||||
</li>
|
|
||||||
<li class="flex gap-x-3">
|
|
||||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
|
||||||
aria-hidden="true">
|
|
||||||
<path fill-rule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
||||||
clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Bring your own S3
|
|
||||||
</li>
|
</li>
|
||||||
<li class="flex gap-x-3">
|
<li class="flex gap-x-3">
|
||||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||||
@ -172,22 +150,29 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-16 lg:px-8 lg:pt-0 xl:px-14">
|
<div class="pt-16 lg:px-8 lg:pt-0 xl:px-14">
|
||||||
<h3 id="tier-essential" class="text-base font-semibold leading-7 text-white">Essential</h3>
|
<h3 id="tier-pro" class="text-base font-semibold leading-7 text-white">Pro</h3>
|
||||||
<p class="flex items-baseline mt-6 gap-x-1">
|
<p class="flex items-baseline mt-6 gap-x-1">
|
||||||
<span x-show="selected === 'monthly'" x-cloak>
|
<span x-show="selected === 'monthly'" x-cloak>
|
||||||
<span class="text-4xl font-bold tracking-tight text-white">$29</span>
|
<span class="text-4xl font-bold tracking-tight text-white">$29</span>
|
||||||
<span class="text-sm font-semibold leading-6 ">/monthly</span>
|
<span class="text-sm font-semibold leading-6 ">/month</span>
|
||||||
</span>
|
</span>
|
||||||
<span x-show="selected === 'yearly'" x-cloak>
|
<span x-show="selected === 'yearly'" x-cloak>
|
||||||
<span class="text-4xl font-bold tracking-tight text-white">$319</span>
|
<span class="text-4xl font-bold tracking-tight text-white">$26</span>
|
||||||
<span class="text-sm font-semibold leading-6 ">/yearly</span>
|
<span class="text-sm font-semibold leading-6 ">/month</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<span x-show="selected === 'yearly'" x-cloak class="text-warning">(save $29)</span>
|
<span x-show="selected === 'monthly'" x-cloak>
|
||||||
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-essential" class="buyme"
|
<span>billed monthly</span>
|
||||||
href="{{ getSubscriptionLink('monthly_pro') }}">Subscribe</a>
|
</span>
|
||||||
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-essential" class="buyme"
|
<span x-show="selected === 'yearly'" x-cloak>
|
||||||
href="{{ getSubscriptionLink('yearly') }}">Subscribe</a>
|
<span>billed annually</span>
|
||||||
|
</span>
|
||||||
|
@if ($showSubscribeButtons)
|
||||||
|
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-pro" class="buyme"
|
||||||
|
href="{{ getSubscriptionLink('monthly_pro') }}">Subscribe</a>
|
||||||
|
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-pro" class="buyme"
|
||||||
|
href="{{ getSubscriptionLink('yearly_pro') }}">Subscribe</a>
|
||||||
|
@endif
|
||||||
<p class="h-20 mt-10 text-sm leading-6 text-white">Scale your business or self-hosting environment.
|
<p class="h-20 mt-10 text-sm leading-6 text-white">Scale your business or self-hosting environment.
|
||||||
</p>
|
</p>
|
||||||
<ul role="list" class="mt-6 space-y-3 text-sm leading-6 ">
|
<ul role="list" class="mt-6 space-y-3 text-sm leading-6 ">
|
||||||
@ -198,25 +183,7 @@
|
|||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||||
clip-rule="evenodd" />
|
clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
5 servers
|
5 servers <x-helper helper="Bring Your Own Server. All you need is n SSH connection." />
|
||||||
</li>
|
|
||||||
<li class="flex gap-x-3">
|
|
||||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
|
||||||
aria-hidden="true">
|
|
||||||
<path fill-rule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
||||||
clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Unlimited Deployments
|
|
||||||
</li>
|
|
||||||
<li class="flex gap-x-3">
|
|
||||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
|
||||||
aria-hidden="true">
|
|
||||||
<path fill-rule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
||||||
clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Provided (optional) S3
|
|
||||||
</li>
|
</li>
|
||||||
<li class="flex gap-x-3">
|
<li class="flex gap-x-3">
|
||||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||||
@ -242,22 +209,29 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-16 lg:px-8 lg:pt-0 xl:px-14">
|
<div class="pt-16 lg:px-8 lg:pt-0 xl:px-14">
|
||||||
<h3 id="tier-growth" class="text-base font-semibold leading-7 text-white">Growth</h3>
|
<h3 id="tier-ultimate" class="text-base font-semibold leading-7 text-white">Ultimate</h3>
|
||||||
<p class="flex items-baseline mt-6 gap-x-1">
|
<p class="flex items-baseline mt-6 gap-x-1">
|
||||||
<span x-show="selected === 'monthly'" x-cloak>
|
<span x-show="selected === 'monthly'" x-cloak>
|
||||||
<span class="text-4xl font-bold tracking-tight text-white">$49</span>
|
<span class="text-4xl font-bold tracking-tight text-white">$69</span>
|
||||||
<span class="text-sm font-semibold leading-6 ">/monthly</span>
|
<span class="text-sm font-semibold leading-6 ">/month</span>
|
||||||
</span>
|
</span>
|
||||||
<span x-show="selected === 'yearly'" x-cloak>
|
<span x-show="selected === 'yearly'" x-cloak>
|
||||||
<span class="text-4xl font-bold tracking-tight text-white">$539</span>
|
<span class="text-4xl font-bold tracking-tight text-white">$63</span>
|
||||||
<span class="text-sm font-semibold leading-6 ">/yearly</span>
|
<span class="text-sm font-semibold leading-6 ">/month</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<span x-show="selected === 'yearly'" x-cloak class="text-warning">(save $69)</span>
|
<span x-show="selected === 'monthly'" x-cloak>
|
||||||
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-growth" class="buyme"
|
<span>billed monthly</span>
|
||||||
href="{{ getSubscriptionLink('monthly_ultimate') }}">Subscribe</a>
|
</span>
|
||||||
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-growth" class="buyme"
|
<span x-show="selected === 'yearly'" x-cloak>
|
||||||
href="{{ getSubscriptionLink('yearly') }}">Subscribe</a>
|
<span>billed annually</span>
|
||||||
|
</span>
|
||||||
|
@if ($showSubscribeButtons)
|
||||||
|
<a x-show="selected === 'monthly'" x-cloak aria-describedby="tier-ultimate" class="buyme"
|
||||||
|
href="{{ getSubscriptionLink('monthly_ultimate') }}">Subscribe</a>
|
||||||
|
<a x-show="selected === 'yearly'" x-cloak aria-describedby="tier-ultimate" class="buyme"
|
||||||
|
href="{{ getSubscriptionLink('yearly_ultimate') }}">Subscribe</a>
|
||||||
|
@endif
|
||||||
<p class="h-20 mt-10 text-sm leading-6 text-white">Deploy complex infrastuctures and
|
<p class="h-20 mt-10 text-sm leading-6 text-white">Deploy complex infrastuctures and
|
||||||
manage them easily in one place.</p>
|
manage them easily in one place.</p>
|
||||||
<ul role="list" class="mt-6 space-y-3 text-sm leading-6 ">
|
<ul role="list" class="mt-6 space-y-3 text-sm leading-6 ">
|
||||||
@ -268,25 +242,7 @@
|
|||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||||
clip-rule="evenodd" />
|
clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
Unlimited servers
|
15 servers <x-helper helper="Bring Your Own Server. All you need is n SSH connection." />
|
||||||
</li>
|
|
||||||
<li class="flex gap-x-3">
|
|
||||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
|
||||||
aria-hidden="true">
|
|
||||||
<path fill-rule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
||||||
clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Unlimited deployments
|
|
||||||
</li>
|
|
||||||
<li class="flex gap-x-3">
|
|
||||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
|
||||||
aria-hidden="true">
|
|
||||||
<path fill-rule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
|
||||||
clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
Provided (optional) S3
|
|
||||||
</li>
|
</li>
|
||||||
<li class="flex font-bold text-white gap-x-3">
|
<li class="flex font-bold text-white gap-x-3">
|
||||||
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"
|
||||||
@ -312,6 +268,9 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="pt-10">Need unlimited servers or official support for your Coolify instance? <a
|
||||||
|
href="https://docs.coollabs.io/contact" class='text-warning'>Contact us.</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
<div class="pb-6">
|
<div class="pb-6">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h1>Server</h1>
|
<h1>Server</h1>
|
||||||
<livewire:server.proxy.status :server="$server" />
|
@if ($server->settings->is_reachable)
|
||||||
|
<livewire:server.proxy.status :server="$server" />
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="subtitle ">{{ data_get($server, 'name') }}</div>
|
<div class="subtitle ">{{ data_get($server, 'name') }}</div>
|
||||||
<nav class="navbar-main">
|
<nav class="navbar-main">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Someone added this email to the Coolify Cloud's waitlist.
|
Someone added this email to the Coolify Cloud's waitlist.
|
||||||
<br>
|
<br>
|
||||||
<a href="{{ $confirmation_url }}">Click here to confirm</a>! The link will expire in {{config('constants.waitlist.confirmation_valid_for_minutes')}} minutes.<br><br>
|
<a href="{{ $confirmation_url }}">Click here to confirm</a>! The link will expire in {{config('constants.waitlist.confirmation_valid_for_minutes')}} minutes.<br><br>
|
||||||
You have no idea what <a href="https://cloud.coolify.io">Coolify Cloud</a> is or this waitlist? <a href="{{ $cancel_url }}">Click here to remove</a> you from the waitlist.
|
You have no idea what <a href="https://coolify.io">Coolify Cloud</a> is or this waitlist? <a href="{{ $cancel_url }}">Click here to remove</a> you from the waitlist.
|
||||||
|
13
resources/views/emails/waitlist-invitation.blade.php
Normal file
13
resources/views/emails/waitlist-invitation.blade.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Congratulations!<br>
|
||||||
|
<br>
|
||||||
|
You have been invited to join the Coolify Cloud. <a href="{{base_url()}}/login">Login here</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Credentials:
|
||||||
|
<br>
|
||||||
|
Email: {{ $email }}
|
||||||
|
<br>
|
||||||
|
Password: {{ $password }}
|
||||||
|
<br>
|
||||||
|
(You will forced to change it on first login.)
|
||||||
|
|
@ -10,42 +10,51 @@
|
|||||||
running on.<br>Please think again.</p>
|
running on.<br>Please think again.</p>
|
||||||
</x-slot:modalBody>
|
</x-slot:modalBody>
|
||||||
</x-modal>
|
</x-modal>
|
||||||
@if ($server->settings->is_reachable)
|
<form wire:submit.prevent='submit' class="flex flex-col">
|
||||||
<form wire:submit.prevent='submit' class="flex flex-col">
|
<div class="flex gap-2">
|
||||||
<div class="flex gap-2">
|
<h2>General</h2>
|
||||||
<h2>General</h2>
|
@if ($server->id === 0)
|
||||||
@if ($server->id === 0)
|
<x-forms.button isModal modalId="changeLocalhost">Save</x-forms.button>
|
||||||
<x-forms.button isModal modalId="changeLocalhost">Save</x-forms.button>
|
@else
|
||||||
@else
|
<x-forms.button type="submit">Save</x-forms.button>
|
||||||
<x-forms.button type="submit">Save</x-forms.button>
|
@endif
|
||||||
@endif
|
<x-forms.button wire:click.prevent='validateServer'>
|
||||||
</div>
|
Validate Server
|
||||||
<div class="flex flex-col gap-2 ">
|
</x-forms.button>
|
||||||
<div class="flex flex-col w-full gap-2 lg:flex-row">
|
</div>
|
||||||
<x-forms.input id="server.name" label="Name" required />
|
@if (!$server->settings->is_reachable)
|
||||||
<x-forms.input id="server.description" label="Description" />
|
You can't use this server until it is validated.
|
||||||
<x-forms.input placeholder="https://example.com" id="wildcard_domain" label="Wildcard Domain"
|
@else
|
||||||
helper="Wildcard domain for your applications. If you set this, you will get a random generated domain for your new applications.<br><span class='font-bold text-white'>Example</span>In case you set:<span class='text-helper'>https://example.com</span>your applications will get: <span class='text-helper'>https://randomId.example.com</span>" />
|
Server validated.
|
||||||
{{-- <x-forms.checkbox disabled type="checkbox" id="server.settings.is_part_of_swarm"
|
@endif
|
||||||
|
<div class="flex flex-col gap-2 pt-4">
|
||||||
|
<div class="flex flex-col w-full gap-2 lg:flex-row">
|
||||||
|
<x-forms.input id="server.name" label="Name" required />
|
||||||
|
<x-forms.input id="server.description" label="Description" />
|
||||||
|
<x-forms.input placeholder="https://example.com" id="wildcard_domain" label="Wildcard Domain"
|
||||||
|
helper="Wildcard domain for your applications. If you set this, you will get a random generated domain for your new applications.<br><span class='font-bold text-white'>Example</span>In case you set:<span class='text-helper'>https://example.com</span>your applications will get: <span class='text-helper'>https://randomId.example.com</span>" />
|
||||||
|
{{-- <x-forms.checkbox disabled type="checkbox" id="server.settings.is_part_of_swarm"
|
||||||
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)
|
@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
|
@else
|
||||||
<x-forms.input id="server.ip" label="IP Address" readonly required />
|
<x-forms.input id="server.ip" label="IP Address" readonly required />
|
||||||
@endif
|
@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 />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
@if ($server->settings->is_reachable)
|
||||||
<h3 class="py-4">Settings</h3>
|
<h3 class="py-4">Settings</h3>
|
||||||
<div class="flex items-center w-64 gap-2">
|
<div class="flex items-center w-64 gap-2">
|
||||||
<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." />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="py-4">Actions</h3>
|
<h3 class="py-4">Actions</h3>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<x-forms.button wire:click.prevent='validateServer'>
|
<x-forms.button wire:click.prevent='validateServer'>
|
||||||
@ -61,26 +70,20 @@
|
|||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="container w-full py-4 mx-auto">
|
@endif
|
||||||
<livewire:activity-monitor header="Logs" />
|
<div class="container w-full py-4 mx-auto">
|
||||||
</div>
|
<livewire:activity-monitor header="Logs" />
|
||||||
@isset($uptime)
|
|
||||||
<h3 class="pb-3">Server Info</h3>
|
|
||||||
<div class="py-2 pb-4">
|
|
||||||
<p>Uptime: {{ $uptime }}</p>
|
|
||||||
@isset($dockerVersion)
|
|
||||||
<p>Docker Engine {{ $dockerVersion }}</p>
|
|
||||||
@endisset
|
|
||||||
</div>
|
|
||||||
@endisset
|
|
||||||
</form>
|
|
||||||
@else
|
|
||||||
<div class="w-full pb-4">
|
|
||||||
<div class="cursor-pointer box" wire:click.prevent='validateServer'>
|
|
||||||
Validate Server
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@isset($uptime)
|
||||||
|
<h3 class="pb-3">Server Info</h3>
|
||||||
|
<div class="py-2 pb-4">
|
||||||
|
<p>Uptime: {{ $uptime }}</p>
|
||||||
|
@isset($dockerVersion)
|
||||||
|
<p>Docker Engine {{ $dockerVersion }}</p>
|
||||||
|
@endisset
|
||||||
|
</div>
|
||||||
|
@endisset
|
||||||
|
</form>
|
||||||
<h2>Danger Zone</h2>
|
<h2>Danger Zone</h2>
|
||||||
<div class="">Woah. I hope you know what are you doing.</div>
|
<div class="">Woah. I hope you know what are you doing.</div>
|
||||||
<h4 class="pt-4">Delete Server</h4>
|
<h4 class="pt-4">Delete Server</h4>
|
||||||
|
31
resources/views/livewire/subscription/actions.blade.php
Normal file
31
resources/views/livewire/subscription/actions.blade.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<div>
|
||||||
|
<div>Status: {{ auth()->user()->currentTeam()->subscription->lemon_status }}</div>
|
||||||
|
<div>Type: {{ auth()->user()->currentTeam()->subscription->lemon_variant_name }}</div>
|
||||||
|
@if (auth()->user()->currentTeam()->subscription->lemon_status === 'cancelled')
|
||||||
|
<div class="pb-4">Subscriptions ends at: {{ getRenewDate() }}</div>
|
||||||
|
<div class="py-4">If you would like to change the subscription to a lower/higher plan, <a
|
||||||
|
class="text-white underline" href="https://docs.coollabs.io/contact" target="_blank">please
|
||||||
|
contact
|
||||||
|
us.</a></div>
|
||||||
|
@else
|
||||||
|
<div class="pb-4">Renews at: {{ getRenewDate() }}</div>
|
||||||
|
@endif
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
@if (auth()->user()->currentTeam()->subscription->lemon_status === 'cancelled')
|
||||||
|
<x-forms.button class="bg-coollabs-gradient" wire:click='resume'>Resume Subscription
|
||||||
|
</x-forms.button>
|
||||||
|
@else
|
||||||
|
<x-forms.button wire:click='cancel'>Cancel Subscription</x-forms.button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<x-forms.button><a class="text-white hover:no-underline" href="{{ getPaymentLink() }}">Update Payment
|
||||||
|
Details</a>
|
||||||
|
</x-forms.button>
|
||||||
|
<a class="text-white hover:no-underline"
|
||||||
|
href="https://app.lemonsqueezy.com/my-orders"><x-forms.button>Manage My
|
||||||
|
Subscription</x-forms.button></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -23,6 +23,9 @@
|
|||||||
<x-forms.input id="email" type="email" label="Email" placeholder="youareawesome@protonmail.com" />
|
<x-forms.input id="email" type="email" label="Email" placeholder="youareawesome@protonmail.com" />
|
||||||
<x-forms.button type="submit">Join Waitlist</x-forms.button>
|
<x-forms.button type="submit">Join Waitlist</x-forms.button>
|
||||||
</form>
|
</form>
|
||||||
Waiting in the line: {{$waiting_in_line}}
|
Waiting in the line: <span class="font-bold text-warning">{{ $waiting_in_line }}</span>
|
||||||
|
<div class="pt-4">
|
||||||
|
See the pricing <a href="https://coolify.io/pricing" class="text-warning">here</a>.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,34 +8,13 @@
|
|||||||
<h2>Subscription</h2>
|
<h2>Subscription</h2>
|
||||||
@if (data_get(auth()->user()->currentTeam(),
|
@if (data_get(auth()->user()->currentTeam(),
|
||||||
'subscription'))
|
'subscription'))
|
||||||
<div>Status: {{ auth()->user()->currentTeam()->subscription->lemon_status }}</div>
|
<livewire:subscription.actions />
|
||||||
<div>Type: {{ auth()->user()->currentTeam()->subscription->lemon_variant_name }}</div>
|
|
||||||
@if (auth()->user()->currentTeam()->subscription->lemon_status === 'cancelled')
|
|
||||||
<div class="pb-4">Subscriptions ends at: {{ getRenewDate() }}</div>
|
|
||||||
<x-forms.button class="bg-coollabs-gradient"><a class="text-white hover:no-underline"
|
|
||||||
href="{{ route('subscription') }}">Resume Subscription</a>
|
|
||||||
</x-forms.button>
|
|
||||||
<div class="py-4">If you would like to change the subscription to a lower/higher plan, <a
|
|
||||||
class="text-white underline" href="https://docs.coollabs.io/contact" target="_blank">please
|
|
||||||
contact
|
|
||||||
us.</a></div>
|
|
||||||
@else
|
|
||||||
<div class="pb-4">Renews at: {{ getRenewDate() }}</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
|
|
||||||
<x-forms.button><a class="text-white hover:no-underline" href="{{ getPaymentLink() }}">Update Payment
|
|
||||||
Details</a>
|
|
||||||
</x-forms.button>
|
|
||||||
@else
|
@else
|
||||||
<x-forms.button class="mt-4"><a class="text-white hover:no-underline"
|
<x-forms.button class="mt-4"><a class="text-white hover:no-underline"
|
||||||
href="{{ route('subscription') }}">Subscribe Now</a>
|
href="{{ route('subscription') }}">Subscribe Now</a>
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
@endif
|
@endif
|
||||||
<x-forms.button><a class="text-white hover:no-underline"
|
|
||||||
href="https://app.lemonsqueezy.com/my-orders">Manage My
|
|
||||||
Subscription</a>
|
|
||||||
</x-forms.button>
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<livewire:team.delete />
|
<livewire:team.delete />
|
||||||
|
@ -174,125 +174,126 @@ Route::post('/source/github/events', function () {
|
|||||||
return general_error_handler(err: $e);
|
return general_error_handler(err: $e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Route::get('/waitlist/confirm', function () {
|
||||||
|
$email = request()->get('email');
|
||||||
|
$confirmation_code = request()->get('confirmation_code');
|
||||||
|
ray($email, $confirmation_code);
|
||||||
|
try {
|
||||||
|
$found = Waitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
|
||||||
|
if ($found && !$found->verified && $found->created_at > now()->subMinutes(config('constants.waitlist.confirmation_valid_for_minutes'))) {
|
||||||
|
$found->verified = true;
|
||||||
|
$found->save();
|
||||||
|
send_internal_notification('Waitlist confirmed: ' . $email);
|
||||||
|
return 'Thank you for confirming your email address. We will notify you when you are next in line.';
|
||||||
|
}
|
||||||
|
return redirect()->route('dashboard');
|
||||||
|
} catch (error) {
|
||||||
|
return redirect()->route('dashboard');
|
||||||
|
}
|
||||||
|
})->name('webhooks.waitlist.confirm');
|
||||||
|
Route::get('/waitlist/cancel', function () {
|
||||||
|
$email = request()->get('email');
|
||||||
|
$confirmation_code = request()->get('confirmation_code');
|
||||||
|
try {
|
||||||
|
$found = Waitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
|
||||||
|
if ($found && !$found->verified) {
|
||||||
|
$found->delete();
|
||||||
|
send_internal_notification('Waitlist cancelled: ' . $email);
|
||||||
|
return 'Your email address has been removed from the waitlist.';
|
||||||
|
}
|
||||||
|
return redirect()->route('dashboard');
|
||||||
|
} catch (error) {
|
||||||
|
return redirect()->route('dashboard');
|
||||||
|
}
|
||||||
|
})->name('webhooks.waitlist.cancel');
|
||||||
|
Route::post('/payments/events', function () {
|
||||||
|
try {
|
||||||
|
$secret = config('coolify.lemon_squeezy_webhook_secret');
|
||||||
|
$payload = request()->collect();
|
||||||
|
$hash = hash_hmac('sha256', $payload, $secret);
|
||||||
|
$signature = request()->header('X-Signature');
|
||||||
|
|
||||||
if (is_cloud()) {
|
if (!hash_equals($hash, $signature)) {
|
||||||
Route::get('/waitlist/confirm', function () {
|
return response('Invalid signature.', 400);
|
||||||
$email = request()->get('email');
|
|
||||||
$confirmation_code = request()->get('confirmation_code');
|
|
||||||
ray($email, $confirmation_code);
|
|
||||||
try {
|
|
||||||
$found = Waitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
|
|
||||||
if ($found && !$found->verified && $found->created_at > now()->subMinutes(config('constants.waitlist.confirmation_valid_for_minutes'))) {
|
|
||||||
$found->verified = true;
|
|
||||||
$found->save();
|
|
||||||
return 'Thank you for confirming your email address. We will notify you when you are next in line.';
|
|
||||||
}
|
|
||||||
return redirect()->route('dashboard');
|
|
||||||
} catch (error) {
|
|
||||||
return redirect()->route('dashboard');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
})->name('webhooks.waitlist.confirm');
|
$webhook = Webhook::create([
|
||||||
Route::get('/waitlist/cancel', function () {
|
'type' => 'lemonsqueezy',
|
||||||
$email = request()->get('email');
|
'payload' => $payload,
|
||||||
$confirmation_code = request()->get('confirmation_code');
|
]);
|
||||||
try {
|
$event = data_get($payload, 'meta.event_name');
|
||||||
$found = Waitlist::where('uuid', $confirmation_code)->where('email', $email)->first();
|
ray('Subscription event: ' . $event);
|
||||||
if ($found && !$found->verified) {
|
$email = data_get($payload, 'data.attributes.user_email');
|
||||||
$found->delete();
|
$team_id = data_get($payload, 'meta.custom_data.team_id');
|
||||||
return 'Your email address has been removed from the waitlist.';
|
if (is_null($team_id) || empty($team_id)) {
|
||||||
}
|
throw new Exception('No team_id found in webhook payload.');
|
||||||
return redirect()->route('dashboard');
|
|
||||||
} catch (error) {
|
|
||||||
return redirect()->route('dashboard');
|
|
||||||
}
|
}
|
||||||
|
$subscription_id = data_get($payload, 'data.id');
|
||||||
})->name('webhooks.waitlist.cancel');
|
$order_id = data_get($payload, 'data.attributes.order_id');
|
||||||
Route::post('/payments/events', function () {
|
$product_id = data_get($payload, 'data.attributes.product_id');
|
||||||
try {
|
$variant_id = data_get($payload, 'data.attributes.variant_id');
|
||||||
$secret = config('coolify.lemon_squeezy_webhook_secret');
|
$variant_name = data_get($payload, 'data.attributes.variant_name');
|
||||||
$payload = request()->collect();
|
$customer_id = data_get($payload, 'data.attributes.customer_id');
|
||||||
$hash = hash_hmac('sha256', $payload, $secret);
|
$status = data_get($payload, 'data.attributes.status');
|
||||||
$signature = request()->header('X-Signature');
|
$trial_ends_at = data_get($payload, 'data.attributes.trial_ends_at');
|
||||||
|
$renews_at = data_get($payload, 'data.attributes.renews_at');
|
||||||
if (!hash_equals($hash, $signature)) {
|
$ends_at = data_get($payload, 'data.attributes.ends_at');
|
||||||
return response('Invalid signature.', 400);
|
$update_payment_method = data_get($payload, 'data.attributes.urls.update_payment_method');
|
||||||
}
|
$team = Team::find($team_id);
|
||||||
|
$found = $team->members->where('email', $email)->first();
|
||||||
$webhook = Webhook::create([
|
if (!$found->isAdmin()) {
|
||||||
'type' => 'lemonsqueezy',
|
throw new Exception("User {$email} is not an admin or owner of team {$team->id}.");
|
||||||
'payload' => $payload,
|
}
|
||||||
]);
|
switch ($event) {
|
||||||
$event = data_get($payload, 'meta.event_name');
|
case 'subscription_created':
|
||||||
ray('Subscription event: ' . $event);
|
case 'subscription_updated':
|
||||||
$email = data_get($payload, 'data.attributes.user_email');
|
case 'subscription_resumed':
|
||||||
$team_id = data_get($payload, 'meta.custom_data.team_id');
|
case 'subscription_unpaused':
|
||||||
if (is_null($team_id) || empty($team_id)) {
|
send_internal_notification("LemonSqueezy Event (`$event`): `" . $email . '` with status `' . $status . '`, tier: `' . $variant_name . '`');
|
||||||
throw new Exception('No team_id found in webhook payload.');
|
$subscription = Subscription::updateOrCreate([
|
||||||
}
|
'team_id' => $team_id,
|
||||||
$subscription_id = data_get($payload, 'data.id');
|
], [
|
||||||
$order_id = data_get($payload, 'data.attributes.order_id');
|
'lemon_subscription_id' => $subscription_id,
|
||||||
$product_id = data_get($payload, 'data.attributes.product_id');
|
'lemon_customer_id' => $customer_id,
|
||||||
$variant_id = data_get($payload, 'data.attributes.variant_id');
|
'lemon_order_id' => $order_id,
|
||||||
$variant_name = data_get($payload, 'data.attributes.variant_name');
|
'lemon_product_id' => $product_id,
|
||||||
$customer_id = data_get($payload, 'data.attributes.customer_id');
|
'lemon_variant_id' => $variant_id,
|
||||||
$status = data_get($payload, 'data.attributes.status');
|
'lemon_status' => $status,
|
||||||
$trial_ends_at = data_get($payload, 'data.attributes.trial_ends_at');
|
'lemon_variant_name' => $variant_name,
|
||||||
$renews_at = data_get($payload, 'data.attributes.renews_at');
|
'lemon_trial_ends_at' => $trial_ends_at,
|
||||||
$ends_at = data_get($payload, 'data.attributes.ends_at');
|
'lemon_renews_at' => $renews_at,
|
||||||
$update_payment_method = data_get($payload, 'data.attributes.urls.update_payment_method');
|
'lemon_ends_at' => $ends_at,
|
||||||
$team = Team::find($team_id);
|
'lemon_update_payment_menthod_url' => $update_payment_method,
|
||||||
$found = $team->members->where('email', $email)->first();
|
]);
|
||||||
if (!$found->isAdmin()) {
|
break;
|
||||||
throw new Exception("User {$email} is not an admin or owner of team {$team->id}.");
|
case 'subscription_cancelled':
|
||||||
}
|
case 'subscription_paused':
|
||||||
switch ($event) {
|
case 'subscription_expired':
|
||||||
case 'subscription_created':
|
$subscription = Subscription::where('team_id', $team_id)->where('lemon_order_id', $order_id)->first();
|
||||||
case 'subscription_updated':
|
if ($subscription) {
|
||||||
case 'subscription_resumed':
|
send_internal_notification("LemonSqueezy Event (`$event`): " . $subscription_id . ' for team ' . $team_id . ' with status ' . $status);
|
||||||
case 'subscription_unpaused':
|
$subscription->update([
|
||||||
$subscription = Subscription::updateOrCreate([
|
|
||||||
'team_id' => $team_id,
|
|
||||||
], [
|
|
||||||
'lemon_subscription_id' => $subscription_id,
|
|
||||||
'lemon_customer_id' => $customer_id,
|
|
||||||
'lemon_order_id' => $order_id,
|
|
||||||
'lemon_product_id' => $product_id,
|
|
||||||
'lemon_variant_id' => $variant_id,
|
|
||||||
'lemon_status' => $status,
|
'lemon_status' => $status,
|
||||||
'lemon_variant_name' => $variant_name,
|
|
||||||
'lemon_trial_ends_at' => $trial_ends_at,
|
'lemon_trial_ends_at' => $trial_ends_at,
|
||||||
'lemon_renews_at' => $renews_at,
|
'lemon_renews_at' => $renews_at,
|
||||||
'lemon_ends_at' => $ends_at,
|
'lemon_ends_at' => $ends_at,
|
||||||
'lemon_update_payment_menthod_url' => $update_payment_method,
|
'lemon_update_payment_menthod_url' => $update_payment_method,
|
||||||
]);
|
]);
|
||||||
break;
|
}
|
||||||
case 'subscription_cancelled':
|
break;
|
||||||
case 'subscription_paused':
|
|
||||||
case 'subscription_expired':
|
|
||||||
$subscription = Subscription::where('team_id', $team_id)->where('lemon_order_id', $order_id)->first();
|
|
||||||
if ($subscription) {
|
|
||||||
$subscription->update([
|
|
||||||
'lemon_status' => $status,
|
|
||||||
'lemon_trial_ends_at' => $trial_ends_at,
|
|
||||||
'lemon_renews_at' => $renews_at,
|
|
||||||
'lemon_ends_at' => $ends_at,
|
|
||||||
'lemon_update_payment_menthod_url' => $update_payment_method,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$webhook->update([
|
|
||||||
'status' => 'success',
|
|
||||||
]);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
ray($e->getMessage());
|
|
||||||
$webhook->update([
|
|
||||||
'status' => 'failed',
|
|
||||||
'failure_reason' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
} finally {
|
|
||||||
return response('OK');
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
$webhook->update([
|
||||||
|
'status' => 'success',
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
ray($e->getMessage());
|
||||||
|
send_internal_notification('Subscription webhook failed: ' . $e->getMessage());
|
||||||
|
$webhook->update([
|
||||||
|
'status' => 'failed',
|
||||||
|
'failure_reason' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
return response('OK');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"version": "3.12.36"
|
"version": "3.12.36"
|
||||||
},
|
},
|
||||||
"v4": {
|
"v4": {
|
||||||
"version": "4.0.0-beta.18"
|
"version": "4.0.0-beta.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user