diff --git a/app/Console/Commands/InviteFromWaitlist.php b/app/Console/Commands/InviteFromWaitlist.php new file mode 100644 index 000000000..4adc60693 --- /dev/null +++ b/app/Console/Commands/InviteFromWaitlist.php @@ -0,0 +1,77 @@ +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. 📧"); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 21c4de1d0..047f037d7 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -3,13 +3,12 @@ namespace App\Console; use App\Jobs\CheckResaleLicenseJob; -use App\Jobs\CheckResaleLicenseKeys; use App\Jobs\CleanupInstanceStuffsJob; use App\Jobs\DatabaseBackupJob; use App\Jobs\DockerCleanupJob; -use App\Jobs\InstanceApplicationsStatusJob; use App\Jobs\InstanceAutoUpdateJob; use App\Jobs\ProxyCheckJob; +use App\Jobs\ResourceStatusJob; use App\Models\ScheduledDatabaseBackup; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; @@ -21,7 +20,7 @@ class Kernel extends ConsoleKernel // $schedule->call(fn() => $this->check_scheduled_backups($schedule))->everyTenSeconds(); if (is_dev()) { $schedule->command('horizon:snapshot')->everyMinute(); - $schedule->job(new InstanceApplicationsStatusJob)->everyMinute(); + $schedule->job(new ResourceStatusJob)->everyMinute(); $schedule->job(new ProxyCheckJob)->everyFiveMinutes(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute(); @@ -31,7 +30,7 @@ class Kernel extends ConsoleKernel } else { $schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute(); - $schedule->job(new InstanceApplicationsStatusJob)->everyMinute(); + $schedule->job(new ResourceStatusJob)->everyMinute(); $schedule->job(new CheckResaleLicenseJob)->hourly(); $schedule->job(new ProxyCheckJob)->everyFiveMinutes(); $schedule->job(new DockerCleanupJob)->everyTenMinutes(); @@ -49,7 +48,10 @@ class Kernel extends ConsoleKernel return; } foreach ($scheduled_backups as $scheduled_backup) { - if (!$scheduled_backup->enabled) continue; + if (!$scheduled_backup->enabled) { + continue; + } + if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; } diff --git a/app/Http/Livewire/PrivateKey/Change.php b/app/Http/Livewire/PrivateKey/Change.php index d40e70e34..91d42bd7a 100644 --- a/app/Http/Livewire/PrivateKey/Change.php +++ b/app/Http/Livewire/PrivateKey/Change.php @@ -43,7 +43,7 @@ class Change extends Component $this->private_key->private_key .= "\n"; } $this->private_key->save(); - refreshPrivateKey($this->private_key); + refresh_server_connection($this->private_key); } catch (\Exception $e) { return general_error_handler(err: $e, that: $this); } diff --git a/app/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php index 8b2ed8900..04be74140 100644 --- a/app/Http/Livewire/Project/Application/General.php +++ b/app/Http/Livewire/Project/Application/General.php @@ -152,7 +152,7 @@ class General extends Component if ($this->application->publish_directory && $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->emit('success', 'Application settings updated!'); } catch (\Exception $e) { diff --git a/app/Http/Livewire/Project/Database/BackupExecution.php b/app/Http/Livewire/Project/Database/BackupExecution.php index f963fa1d6..2f9d7dcb5 100644 --- a/app/Http/Livewire/Project/Database/BackupExecution.php +++ b/app/Http/Livewire/Project/Database/BackupExecution.php @@ -17,7 +17,7 @@ class BackupExecution extends Component { delete_backup_locally($this->execution->filename, $this->execution->scheduledDatabaseBackup->database->destination->server); $this->execution->delete(); - $this->emit('success', 'Backup execution deleted successfully.'); + $this->emit('success', 'Backup deleted successfully.'); $this->emit('refreshBackupExecutions'); } } diff --git a/app/Http/Livewire/Server/PrivateKey.php b/app/Http/Livewire/Server/PrivateKey.php index 366aec85f..8f433f73d 100644 --- a/app/Http/Livewire/Server/PrivateKey.php +++ b/app/Http/Livewire/Server/PrivateKey.php @@ -17,7 +17,7 @@ class PrivateKey extends Component $this->server->update([ 'private_key_id' => $private_key_id ]); - refreshPrivateKey($this->server->privateKey); + refresh_server_connection($this->server->privateKey); $this->server->refresh(); $this->checkConnection(); } diff --git a/app/Http/Livewire/Subscription/Actions.php b/app/Http/Livewire/Subscription/Actions.php new file mode 100644 index 000000000..588a0521c --- /dev/null +++ b/app/Http/Livewire/Subscription/Actions.php @@ -0,0 +1,72 @@ +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); + } + } +} diff --git a/app/Http/Livewire/Waitlist.php b/app/Http/Livewire/Waitlist.php index a0d4284b6..de633ceac 100644 --- a/app/Http/Livewire/Waitlist.php +++ b/app/Http/Livewire/Waitlist.php @@ -18,7 +18,7 @@ class Waitlist extends Component public function mount() { if (is_dev()) { - $this->email = 'test@example.com'; + $this->email = 'waitlist@example.com'; } } public function submit() @@ -27,8 +27,7 @@ class Waitlist extends Component try { $already_registered = User::whereEmail($this->email)->first(); if ($already_registered) { - $this->emit('success', 'You are already registered (Thank you 💜).'); - return; + throw new \Exception('You are already on the waitlist or registered.
Please check your email to verify your email address or contact support.'); } $found = ModelsWaitlist::where('email', $this->email)->first(); if ($found) { @@ -36,7 +35,7 @@ class Waitlist extends Component $this->emit('error', 'You are already on the waitlist.
Please check your email to verify your email address.'); return; } - $this->emit('error', 'You are already on the waitlist.'); + $this->emit('error', 'You are already on the waitlist.
You will be notified when your turn comes.
Thank you.'); return; } $waitlist = ModelsWaitlist::create([ @@ -44,11 +43,10 @@ class Waitlist extends Component '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)); } catch (\Exception $e) { return general_error_handler(err: $e, that: $this); } - } } diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php index 7b3be1a44..e67a562bd 100644 --- a/app/Jobs/CleanupInstanceStuffsJob.php +++ b/app/Jobs/CleanupInstanceStuffsJob.php @@ -29,6 +29,7 @@ class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique try { $this->cleanup_waitlist(); } catch (\Exception $e) { + send_internal_notification('CleanupInstanceStuffsJob failed with error: ' . $e->getMessage()); ray($e->getMessage()); } } diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index 083023929..f2be8b4d7 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -35,7 +35,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeUnique { try { $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)); } diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 3fdbea216..eed6c5de8 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -18,6 +18,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Throwable; +use Illuminate\Support\Str; class DatabaseBackupJob implements ShouldQueue { @@ -68,11 +69,13 @@ class DatabaseBackupJob implements ShouldQueue return; } $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') { $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_location = $this->backup_dir . $this->backup_file; @@ -95,6 +98,7 @@ class DatabaseBackupJob implements ShouldQueue private function backup_standalone_postgresql(): void { try { + ray($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"; diff --git a/app/Jobs/InstanceApplicationsStatusJob.php b/app/Jobs/ResourceStatusJob.php similarity index 92% rename from app/Jobs/InstanceApplicationsStatusJob.php rename to app/Jobs/ResourceStatusJob.php index fd173215b..5ac162d3f 100644 --- a/app/Jobs/InstanceApplicationsStatusJob.php +++ b/app/Jobs/ResourceStatusJob.php @@ -10,7 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class InstanceApplicationsStatusJob implements ShouldQueue, ShouldBeUnique +class ResourceStatusJob implements ShouldQueue, ShouldBeUnique { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; diff --git a/app/Jobs/SendConfirmationForWaitlistJob.php b/app/Jobs/SendConfirmationForWaitlistJob.php index 31098e180..3ff4982d9 100755 --- a/app/Jobs/SendConfirmationForWaitlistJob.php +++ b/app/Jobs/SendConfirmationForWaitlistJob.php @@ -24,10 +24,6 @@ class SendConfirmationForWaitlistJob implements ShouldQueue public function handle() { try { - $settings = InstanceSettings::get(); - - - set_transanctional_email_settings($settings); $mail = new MailMessage(); $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, ]); $mail->subject('You are on the waitlist!'); - Mail::send( - [], - [], - 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()) - ); + send_user_an_email($mail, $this->email); } catch (\Throwable $th) { + send_internal_notification('SendConfirmationForWaitlistJob failed with error: ' . $th->getMessage()); ray($th->getMessage()); throw $th; } diff --git a/app/Models/User.php b/app/Models/User.php index f0a85d182..b048ef9e6 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,7 +4,6 @@ namespace App\Models; use App\Notifications\Channels\SendsEmail; use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword; -use App\Notifications\TrnsactionalEmails\ResetPassword; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php new file mode 100644 index 000000000..ee13a6cc2 --- /dev/null +++ b/app/Notifications/Internal/GeneralNotification.php @@ -0,0 +1,27 @@ +message; + } +} diff --git a/app/Notifications/Server/NotReachable.php b/app/Notifications/Server/NotReachable.php new file mode 100644 index 000000000..0b5f71569 --- /dev/null +++ b/app/Notifications/Server/NotReachable.php @@ -0,0 +1,58 @@ +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; + } +} diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index da9282e13..eee442390 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -8,6 +8,7 @@ use App\Actions\Fortify\UpdateUserPassword; use App\Actions\Fortify\UpdateUserProfileInformation; use App\Models\InstanceSettings; use App\Models\User; +use App\Models\Waitlist; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; @@ -45,15 +46,19 @@ class FortifyServiceProvider extends ServiceProvider Fortify::createUsersUsing(CreateNewUser::class); Fortify::registerView(function () { - ray('asd'); $settings = InstanceSettings::get(); + $waiting_in_line = Waitlist::whereVerified(true)->count(); if (!$settings->is_registration_enabled) { return redirect()->route('login'); } if (config('coolify.waitlist')) { - return view('auth.waitlist'); + return view('auth.waitlist',[ + 'waiting_in_line' => $waiting_in_line, + ]); } else { - return view('auth.register'); + return view('auth.register',[ + 'waiting_in_line' => $waiting_in_line, + ]); } }); @@ -75,6 +80,8 @@ class FortifyServiceProvider extends ServiceProvider $user && Hash::check($request->password, $user->password) ) { + $user->updated_at = now(); + $user->save(); session(['currentTeam' => $user->currentTeam = $user->teams->firstWhere('personal_team', true)]); return $user; } diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 2787f153e..7b78abe8a 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -2,6 +2,7 @@ use App\Models\Server; use Illuminate\Support\Collection; +use Illuminate\Support\Str; 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) { + check_server_connection($server); $container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError); if (!$container) { return 'exited'; @@ -53,7 +55,7 @@ function get_container_status(Server $server, string $container_id, bool $all_da if ($all_data) { 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) @@ -66,11 +68,17 @@ function generate_container_name(string $uuid, int $pull_request_id = 0) } function get_port_from_dockerfile($dockerfile): int { - $port = preg_grep('/EXPOSE\s+(\d+)/', explode("\n", $dockerfile)); - if (count($port) > 0 && preg_match('/EXPOSE\s+(\d+)/', $port[1], $matches)) { - $port = $matches[1]; - } else { - $port = 80; + $dockerfile_array = explode("\n", $dockerfile); + $found_exposed_port = null; + foreach ($dockerfile_array as $line) { + $line_str = Str::of($line)->trim(); + 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; } diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 3f8f567d8..ecc679b3f 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -7,6 +7,7 @@ use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\PrivateKey; use App\Models\Server; +use App\Notifications\Server\NotReachable; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; @@ -109,8 +110,8 @@ function instant_remote_process(array $command, Server $server, $throwError = tr $exitCode = $process->exitCode(); if ($exitCode !== 0) { if ($repeat > 1) { + ray("repeat: ", $repeat); Sleep::for(200)->milliseconds(); - ray('executing again'); return instant_remote_process($command, $server, $throwError, $repeat - 1); } // ray('ERROR OCCURED: ' . $process->errorOutput()); @@ -152,12 +153,13 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d return $formatted; } -function refreshPrivateKey(PrivateKey $private_key) +function refresh_server_connection(PrivateKey $private_key) { foreach ($private_key->servers as $server) { // Delete the old ssh mux file to force a new one to be created Storage::disk('ssh-mux')->delete($server->muxFilename()); - 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(); } } @@ -166,7 +168,7 @@ function refreshPrivateKey(PrivateKey $private_key) function validateServer(Server $server) { try { - refreshPrivateKey($server->privateKey); + refresh_server_connection($server->privateKey); $uptime = instant_remote_process(['uptime'], $server); if (!$uptime) { $uptime = 'Server not reachable.'; @@ -192,3 +194,26 @@ function validateServer(Server $server) $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(); + } +} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index ff0e99679..4b470cb48 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1,14 +1,19 @@ 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 { ray('ERROR OCCURRED: ' . $err->getMessage()); @@ -47,9 +52,9 @@ function general_error_handler(Throwable|null $err = null, $that = null, $isJson } else { 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."); - }else { + } else { throw new Exception($customErrorMessage ?? $err->getMessage()); } } catch (Throwable $error) { @@ -104,7 +109,7 @@ function is_transactional_emails_active(): bool 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) { $settings = InstanceSettings::get(); @@ -130,10 +135,16 @@ function set_transanctional_email_settings(InstanceSettings|null $settings = nul function base_ip(): string { if (is_dev()) { - return "http://localhost"; + return "localhost"; } $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; } +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()) + ); +} diff --git a/config/coolify.php b/config/coolify.php index b9e092175..0276b9a78 100644 --- a/config/coolify.php +++ b/config/coolify.php @@ -4,6 +4,7 @@ return [ 'self_hosted' => env('SELF_HOSTED', true), 'waitlist' => env('WAITLIST', false), '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_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), diff --git a/config/version.php b/config/version.php index 8ecc24277..7764582fa 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ integer('unreachable_count')->default(0); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('unreachable_count'); + }); + } +}; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 063d1e257..4a9e0c192 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -33,6 +33,7 @@ services: - PHP_PM_MIN_SPARE_SERVERS=1 - PHP_PM_MAX_SPARE_SERVERS=10 - SELF_HOSTED + - WAITLIST - LEMON_SQUEEZY_WEBHOOK_SECRET - LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_BASIC - LEMON_SQUEEZY_CHECKOUT_ID_MONTHLY_PRO diff --git a/public/coolify-transparent.png b/public/coolify-transparent.png new file mode 100644 index 000000000..22d337a1e Binary files /dev/null and b/public/coolify-transparent.png differ diff --git a/public/coolify.png b/public/coolify.png new file mode 100644 index 000000000..fa01fec05 Binary files /dev/null and b/public/coolify.png differ diff --git a/resources/js/components/MagicBar.vue b/resources/js/components/MagicBar.vue index 0626f16f1..f0203b885 100644 --- a/resources/js/components/MagicBar.vue +++ b/resources/js/components/MagicBar.vue @@ -128,6 +128,15 @@ +