diff --git a/app/Actions/Proxy/SaveConfigurationSync.php b/app/Actions/Proxy/SaveConfigurationSync.php index c17607516..b35b4375b 100644 --- a/app/Actions/Proxy/SaveConfigurationSync.php +++ b/app/Actions/Proxy/SaveConfigurationSync.php @@ -9,15 +9,20 @@ class SaveConfigurationSync { public function __invoke(Server $server, string $configuration) { - $proxy_path = get_proxy_path(); - $docker_compose_yml_base64 = base64_encode($configuration); + try { + $proxy_path = get_proxy_path(); + $docker_compose_yml_base64 = base64_encode($configuration); - $server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; - $server->save(); + $server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; + $server->save(); + + instant_remote_process([ + "mkdir -p $proxy_path", + "echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml", + ], $server); + } catch (\Throwable $th) { + ray($th); + } - instant_remote_process([ - "mkdir -p $proxy_path", - "echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml", - ], $server); } } diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index b9d9ab02b..d541921be 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -12,12 +12,6 @@ class StartProxy { public function __invoke(Server $server): Activity { - // TODO: check for other proxies - if (is_null(data_get($server, 'proxy.type'))) { - $server->proxy->type = ProxyTypes::TRAEFIK_V2->value; - $server->proxy->status = ProxyStatus::EXITED->value; - $server->save(); - } $proxy_path = get_proxy_path(); $networks = collect($server->standaloneDockers)->map(function ($docker) { return $docker['network']; diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index a26fadc05..a7408b2cc 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -37,11 +37,14 @@ public function __invoke(Server $server, Team $team) "docker network create --attachable coolify", "echo ####### Done!" ], $server); - StandaloneDocker::create([ - 'name' => 'coolify', - 'network' => 'coolify', - 'server_id' => $server->id, - ]); + $found = StandaloneDocker::where('server_id', $server->id); + if ($found->count() == 0) { + StandaloneDocker::create([ + 'name' => 'coolify', + 'network' => 'coolify', + 'server_id' => $server->id, + ]); + } } diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index 0be05894f..de4700b75 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -7,9 +7,9 @@ class UpdateCoolify { - public Server $server; - public string $latest_version; - public string $current_version; + public ?Server $server = null; + public ?string $latestVersion = null; + public ?string $currentVersion = null; public function __invoke(bool $force) { @@ -17,13 +17,16 @@ public function __invoke(bool $force) $settings = InstanceSettings::get(); ray('Running InstanceAutoUpdateJob'); $localhost_name = 'localhost'; - $this->server = Server::where('name', $localhost_name)->firstOrFail(); - $this->latest_version = get_latest_version_of_coolify(); - $this->current_version = config('version'); - ray('latest version:' . $this->latest_version . " current version: " . $this->current_version . ' force: ' . $force); + $this->server = Server::where('name', $localhost_name)->first(); + if (!$this->server) { + return; + } + $this->latestVersion = get_latest_version_of_coolify(); + $this->currentVersion = config('version'); + ray('latest version:' . $this->latestVersion . " current version: " . $this->currentVersion . ' force: ' . $force); if ($settings->next_channel) { ray('next channel enabled'); - $this->latest_version = 'next'; + $this->latestVersion = 'next'; } if ($force) { $this->update(); @@ -31,15 +34,15 @@ public function __invoke(bool $force) if (!$settings->is_auto_update_enabled) { return 'Auto update is disabled'; } - if ($this->latest_version === $this->current_version) { + if ($this->latestVersion === $this->currentVersion) { return 'Already on latest version'; } - if (version_compare($this->latest_version, $this->current_version, '<')) { + if (version_compare($this->latestVersion, $this->currentVersion, '<')) { return 'Latest version is lower than current version?!'; } $this->update(); } - send_internal_notification('InstanceAutoUpdateJob done to version: ' . $this->latest_version . ' from version: ' . $this->current_version); + send_internal_notification('InstanceAutoUpdateJob done to version: ' . $this->latestVersion . ' from version: ' . $this->currentVersion); } catch (\Exception $th) { ray('InstanceAutoUpdateJob failed'); ray($th->getMessage()); @@ -51,7 +54,7 @@ public function __invoke(bool $force) private function update() { if (isDev()) { - ray("Running update on local docker container. Updating to $this->latest_version"); + ray("Running update on local docker container. Updating to $this->latestVersion"); remote_process([ "sleep 10" ], $this->server); @@ -61,7 +64,7 @@ private function update() ray('Running update on production server'); remote_process([ "curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh", - "bash /data/coolify/source/upgrade.sh $this->latest_version" + "bash /data/coolify/source/upgrade.sh $this->latestVersion" ], $this->server); return; } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 0dc99ba8a..07032e1a1 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -25,15 +25,15 @@ protected function schedule(Schedule $schedule): void $schedule->job(new CleanupInstanceStuffsJob)->everyMinute(); // $schedule->job(new CheckResaleLicenseJob)->hourly(); - // $schedule->job(new DockerCleanupJob)->everyOddHour(); + $schedule->job(new DockerCleanupJob)->everyOddHour(); // $schedule->job(new InstanceAutoUpdateJob(true))->everyMinute(); } else { $schedule->command('horizon:snapshot')->everyFiveMinutes(); - $schedule->job(new CleanupInstanceStuffsJob)->everyMinute(); - $schedule->job(new ResourceStatusJob)->everyMinute(); - $schedule->job(new CheckResaleLicenseJob)->hourly(); - $schedule->job(new ProxyCheckJob)->everyFiveMinutes(); - $schedule->job(new DockerCleanupJob)->everyTenMinutes(); + $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); + $schedule->job(new ResourceStatusJob)->everyMinute()->onOneServer(); + $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); + $schedule->job(new ProxyCheckJob)->everyFiveMinutes()->onOneServer(); + $schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer(); $schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes(); } $this->check_scheduled_backups($schedule); diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 4fd997055..39b0881e3 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -5,11 +5,9 @@ use App\Models\InstanceSettings; use App\Models\Project; use App\Models\S3Storage; -use App\Models\Server; use App\Models\StandalonePostgresql; use App\Models\TeamInvitation; use App\Models\User; -use App\Models\Waitlist; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Routing\Controller as BaseController; @@ -19,25 +17,19 @@ class Controller extends BaseController { use AuthorizesRequests, ValidatesRequests; - public function waitlist() { - $waiting_in_line = Waitlist::whereVerified(true)->count(); - return view('auth.waitlist', [ - 'waiting_in_line' => $waiting_in_line, - ]); - } public function subscription() { - if (!is_cloud()) { + if (!isCloud()) { abort(404); } - return view('subscription.show', [ + return view('subscription.index', [ 'settings' => InstanceSettings::get(), ]); } public function license() { - if (!is_cloud()) { + if (!isCloud()) { abort(404); } return view('settings.license', [ @@ -48,23 +40,6 @@ public function license() public function force_passoword_reset() { return view('auth.force-password-reset'); } - public function dashboard() - { - $projects = Project::ownedByCurrentTeam()->get(); - $servers = Server::ownedByCurrentTeam()->get(); - $s3s = S3Storage::ownedByCurrentTeam()->get(); - $resources = 0; - foreach ($projects as $project) { - $resources += $project->applications->count(); - $resources += $project->postgresqls->count(); - } - return view('dashboard', [ - 'servers' => $servers->count(), - 'projects' => $projects->count(), - 'resources' => $resources, - 's3s' => $s3s, - ]); - } public function boarding() { if (currentTeam()->boarding || isDev()) { return view('boarding'); @@ -97,7 +72,7 @@ public function team() if (auth()->user()->isAdminFromSession()) { $invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get(); } - return view('team.show', [ + return view('team.index', [ 'invitations' => $invitations, ]); } @@ -146,7 +121,7 @@ public function acceptInvitation() if ($diff <= config('constants.invitation.link.expiration')) { $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); $invitation->delete(); - return redirect()->route('team.show'); + return redirect()->route('team.index'); } else { $invitation->delete(); abort(401); @@ -168,7 +143,7 @@ public function revokeInvitation() abort(401); } $invitation->delete(); - return redirect()->route('team.show'); + return redirect()->route('team.index'); } catch (Throwable $th) { throw $th; } diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 476d54437..07e4a2a0f 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -43,6 +43,7 @@ public function new() { $type = request()->query('type'); $destination_uuid = request()->query('destination'); + $server = requesT()->query('server'); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); if (!$project) { @@ -59,6 +60,9 @@ public function new() 'environment_name' => $environment->name, 'database_uuid' => $standalone_postgresql->uuid, ]); + } + if ($server) { + } return view('project.new', [ 'type' => $type diff --git a/app/Http/Controllers/ServerController.php b/app/Http/Controllers/ServerController.php index 54b8c7a8c..21ca2ba77 100644 --- a/app/Http/Controllers/ServerController.php +++ b/app/Http/Controllers/ServerController.php @@ -12,20 +12,21 @@ class ServerController extends Controller public function new_server() { - if (!is_cloud() || isInstanceAdmin()) { + $privateKeys = PrivateKey::ownedByCurrentTeam()->get(); + if (!isCloud()) { return view('server.create', [ 'limit_reached' => false, - 'private_keys' => PrivateKey::ownedByCurrentTeam()->get(), + 'private_keys' => $privateKeys, ]); } - $servers = currentTeam()->servers->count(); - $subscription = currentTeam()?->subscription->type(); - $your_limit = config('constants.limits.server')[strtolower($subscription)]; - $limit_reached = $servers >= $your_limit; + $team = currentTeam(); + $servers = $team->servers->count(); + ['serverLimit' => $serverLimit] = $team->limits; + $limit_reached = $servers >= $serverLimit; return view('server.create', [ 'limit_reached' => $limit_reached, - 'private_keys' => PrivateKey::ownedByCurrentTeam()->get(), + 'private_keys' => $privateKeys, ]); } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index eb93b6d35..be63b9694 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -38,7 +38,7 @@ class Kernel extends HttpKernel \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, \App\Http\Middleware\CheckForcePasswordReset::class, - \App\Http\Middleware\SubscriptionValid::class, + \App\Http\Middleware\IsSubscriptionValid::class, \App\Http\Middleware\IsBoardingFlow::class, ], diff --git a/app/Http/Livewire/Boarding.php b/app/Http/Livewire/Boarding/Index.php similarity index 65% rename from app/Http/Livewire/Boarding.php rename to app/Http/Livewire/Boarding/Index.php index 7ec11a3bc..35f7fd218 100644 --- a/app/Http/Livewire/Boarding.php +++ b/app/Http/Livewire/Boarding/Index.php @@ -1,17 +1,20 @@ remoteServerHost = 'coolify-testing-host'; } } + public function welcome() { + if (isCloud()) { + return $this->setServerType('remote'); + } + $this->currentState = 'select-server-type'; + } public function restartBoarding() { if ($this->createdServer) { @@ -63,20 +76,62 @@ public function skipBoarding() refreshSession(); return redirect()->route('dashboard'); } - public function setServer(string $type) + + public function setServerType(string $type) { if ($type === 'localhost') { $this->createdServer = Server::find(0); if (!$this->createdServer) { return $this->emit('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.'); } - $this->currentState = 'select-proxy'; + return $this->validateServer(); } elseif ($type === 'remote') { + $this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); + if ($this->privateKeys->count() > 0) { + $this->selectedExistingPrivateKey = $this->privateKeys->first()->id; + } + $this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); + if ($this->servers->count() > 0) { + $this->selectedExistingServer = $this->servers->first()->id; + $this->currentState = 'select-existing-server'; + return; + } $this->currentState = 'private-key'; } } + public function selectExistingServer() + { + $this->createdServer = Server::find($this->selectedExistingServer); + if (!$this->createdServer) { + $this->emit('error', 'Server is not found.'); + $this->currentState = 'private-key'; + return; + } + $this->selectedExistingPrivateKey = $this->createdServer->privateKey->id; + $this->validateServer(); + $this->getProxyType(); + $this->getProjects(); + } + public function getProxyType() { + $proxyTypeSet = $this->createdServer->proxy->type; + if (!$proxyTypeSet) { + $this->currentState = 'select-proxy'; + return; + } + $this->getProjects(); + } + public function selectExistingPrivateKey() + { + $this->currentState = 'create-server'; + } + public function createNewServer() + { + $this->selectedExistingServer = null; + $this->currentState = 'private-key'; + } public function setPrivateKey(string $type) { + $this->selectedExistingPrivateKey = null; $this->privateKeyType = $type; if ($type === 'create' && !isDev()) { $this->createNewPrivateKey(); @@ -115,11 +170,12 @@ public function saveServer() 'private_key_id' => $this->createdPrivateKey->id, 'team_id' => currentTeam()->id ]); + $this->validateServer(); + } + public function validateServer() { try { ['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->createdServer); if (!$uptime) { - $this->createdServer->delete(); - $this->createdPrivateKey->delete(); throw new \Exception('Server is not reachable.'); } else { $this->createdServer->settings->update([ @@ -127,11 +183,14 @@ public function saveServer() ]); $this->emit('success', 'Server is reachable.'); } - if ($dockerVersion) { + ray($dockerVersion, $uptime); + if (!$dockerVersion) { $this->emit('error', 'Docker is not installed on the server.'); $this->currentState = 'install-docker'; return; } + $this->getProxyType(); + } catch (\Exception $e) { return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this); } @@ -145,13 +204,25 @@ public function installDocker() public function selectProxy(string|null $proxyType = null) { if (!$proxyType) { - return $this->currentState = 'create-project'; + return $this->getProjects(); } $this->createdServer->proxy->type = $proxyType; $this->createdServer->proxy->status = 'exited'; $this->createdServer->save(); + $this->getProjects(); + } + + public function getProjects() { + $this->projects = Project::ownedByCurrentTeam(['name'])->get(); + if ($this->projects->count() > 0) { + $this->selectedExistingProject = $this->projects->first()->id; + } $this->currentState = 'create-project'; } + public function selectExistingProject() { + $this->createdProject = Project::find($this->selectedExistingProject); + $this->currentState = 'create-resource'; + } public function createNewProject() { $this->createdProject = Project::create([ @@ -168,7 +239,7 @@ public function showNewResource() [ 'project_uuid' => $this->createdProject->uuid, 'environment_name' => 'production', - + 'server'=> $this->createdServer->id, ] ); } @@ -176,6 +247,10 @@ private function createNewPrivateKey() { $this->privateKeyName = generate_random_name(); $this->privateKeyDescription = 'Created by Coolify'; - ['private' => $this->privateKey, 'public'=> $this->publicKey] = generateSSHKey(); + ['private' => $this->privateKey, 'public' => $this->publicKey] = generateSSHKey(); + } + public function render() + { + return view('livewire.boarding.index')->layout('layouts.boarding'); } } diff --git a/app/Http/Livewire/Dashboard.php b/app/Http/Livewire/Dashboard.php new file mode 100644 index 000000000..874e389e0 --- /dev/null +++ b/app/Http/Livewire/Dashboard.php @@ -0,0 +1,32 @@ +servers = Server::ownedByCurrentTeam()->get()->count(); + $this->s3s = S3Storage::ownedByCurrentTeam()->get()->count(); + $projects = Project::ownedByCurrentTeam()->get(); + foreach ($projects as $project) { + $this->resources += $project->applications->count(); + $this->resources += $project->postgresqls->count(); + } + $this->projects = $projects->count(); + } + public function render() + { + return view('livewire.dashboard'); + } +} diff --git a/app/Http/Livewire/Dev/S3Test.php b/app/Http/Livewire/Dev/S3Test.php deleted file mode 100644 index 3a20224cd..000000000 --- a/app/Http/Livewire/Dev/S3Test.php +++ /dev/null @@ -1,41 +0,0 @@ -s3 = S3Storage::first(); - } - - public function save() - { - try { - $this->validate([ - 'file' => 'required|max:150', // 1MB Max - ]); - set_s3_target($this->s3); - $this->file->storeAs('files', $this->file->getClientOriginalName(), 'custom-s3'); - $this->emit('success', 'File uploaded successfully.'); - } catch (\Throwable $th) { - return general_error_handler($th, $this, false); - } - } - - public function get_files() - { - set_s3_target($this->s3); - dd(Storage::disk('custom-s3')->files('files')); - } -} diff --git a/app/Http/Livewire/Notifications/EmailSettings.php b/app/Http/Livewire/Notifications/EmailSettings.php index a2887f989..bf805e5ec 100644 --- a/app/Http/Livewire/Notifications/EmailSettings.php +++ b/app/Http/Livewire/Notifications/EmailSettings.php @@ -6,55 +6,143 @@ use App\Models\Team; use App\Notifications\Test; use Livewire\Component; +use Log; class EmailSettings extends Component { - public Team $model; + public Team $team; public string $emails; + public bool $sharedEmailEnabled = false; protected $rules = [ - 'model.smtp_enabled' => 'nullable|boolean', - 'model.smtp_from_address' => 'required|email', - 'model.smtp_from_name' => 'required', - 'model.smtp_recipients' => 'nullable', - 'model.smtp_host' => 'required', - 'model.smtp_port' => 'required', - 'model.smtp_encryption' => 'nullable', - 'model.smtp_username' => 'nullable', - 'model.smtp_password' => 'nullable', - 'model.smtp_timeout' => 'nullable', - 'model.smtp_notifications_test' => 'nullable|boolean', - 'model.smtp_notifications_deployments' => 'nullable|boolean', - 'model.smtp_notifications_status_changes' => 'nullable|boolean', - 'model.smtp_notifications_database_backups' => 'nullable|boolean', + 'team.smtp_enabled' => 'nullable|boolean', + 'team.smtp_from_address' => 'required|email', + 'team.smtp_from_name' => 'required', + 'team.smtp_recipients' => 'nullable', + 'team.smtp_host' => 'required', + 'team.smtp_port' => 'required', + 'team.smtp_encryption' => 'nullable', + 'team.smtp_username' => 'nullable', + 'team.smtp_password' => 'nullable', + 'team.smtp_timeout' => 'nullable', + 'team.smtp_notifications_test' => 'nullable|boolean', + 'team.smtp_notifications_deployments' => 'nullable|boolean', + 'team.smtp_notifications_status_changes' => 'nullable|boolean', + 'team.smtp_notifications_database_backups' => 'nullable|boolean', + 'team.use_instance_email_settings' => 'boolean', + 'team.resend_enabled' => 'nullable|boolean', + 'team.resend_api_key' => 'nullable', ]; protected $validationAttributes = [ - 'model.smtp_from_address' => 'From Address', - 'model.smtp_from_name' => 'From Name', - 'model.smtp_recipients' => 'Recipients', - 'model.smtp_host' => 'Host', - 'model.smtp_port' => 'Port', - 'model.smtp_encryption' => 'Encryption', - 'model.smtp_username' => 'Username', - 'model.smtp_password' => 'Password', + 'team.smtp_from_address' => 'From Address', + 'team.smtp_from_name' => 'From Name', + 'team.smtp_recipients' => 'Recipients', + 'team.smtp_host' => 'Host', + 'team.smtp_port' => 'Port', + 'team.smtp_encryption' => 'Encryption', + 'team.smtp_username' => 'Username', + 'team.smtp_password' => 'Password', + 'team.smtp_timeout' => 'Timeout', + 'team.resend_enabled' => 'Resend Enabled', + 'team.resend_api_key' => 'Resend API Key', ]; public function mount() { - $this->decrypt(); + ['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits; $this->emails = auth()->user()->email; } - - private function decrypt() + public function submitFromFields() { - if (data_get($this->model, 'smtp_password')) { - try { - $this->model->smtp_password = decrypt($this->model->smtp_password); - } catch (\Exception $e) { + try { + $this->resetErrorBag(); + $this->validate([ + 'team.smtp_from_address' => 'required|email', + 'team.smtp_from_name' => 'required', + ]); + $this->team->save(); + $this->emit('success', 'Settings saved successfully.'); + } catch (\Exception $e) { + return general_error_handler($e, $this); + } + } + public function sendTestNotification() + { + $this->team->notify(new Test($this->emails)); + $this->emit('success', 'Test Email sent successfully.'); + } + public function instantSaveInstance() + { + try { + if (!$this->sharedEmailEnabled) { + throw new \Exception('Not allowed to change settings. Please upgrade your subscription.'); } + $this->team->smtp_enabled = false; + $this->team->resend_enabled = false; + $this->team->save(); + $this->emit('success', 'Settings saved successfully.'); + } catch (\Exception $e) { + return general_error_handler($e, $this); } } + public function instantSaveResend() + { + try { + $this->team->smtp_enabled = false; + $this->submitResend(); + } catch (\Exception $e) { + $this->team->smtp_enabled = false; + return general_error_handler($e, $this); + } + } + public function instantSave() + { + try { + $this->team->resend_enabled = false; + $this->submit(); + } catch (\Exception $e) { + $this->team->smtp_enabled = false; + return general_error_handler($e, $this); + } + } + + public function submit() + { + try { + $this->resetErrorBag(); + $this->validate([ + 'team.smtp_from_address' => 'required|email', + 'team.smtp_from_name' => 'required', + 'team.smtp_host' => 'required', + 'team.smtp_port' => 'required|numeric', + 'team.smtp_encryption' => 'nullable', + 'team.smtp_username' => 'nullable', + 'team.smtp_password' => 'nullable', + 'team.smtp_timeout' => 'nullable', + ]); + $this->team->save(); + $this->emit('success', 'Settings saved successfully.'); + } catch (\Exception $e) { + $this->team->smtp_enabled = false; + return general_error_handler($e, $this); + } + } + public function submitResend() + { + try { + $this->resetErrorBag(); + $this->validate([ + 'team.resend_api_key' => 'required' + ]); + $this->team->save(); + refreshSession(); + $this->emit('success', 'Settings saved successfully.'); + } catch (\Exception $e) { + $this->team->resend_enabled = false; + return general_error_handler($e, $this); + } + } public function copyFromInstanceSettings() { $settings = InstanceSettings::get(); @@ -72,55 +160,22 @@ public function copyFromInstanceSettings() 'smtp_password' => $settings->smtp_password, 'smtp_timeout' => $settings->smtp_timeout, ]); - $this->decrypt(); - if (is_a($team, Team::class)) { - refreshSession(); - } - $this->model = $team; - $this->emit('success', 'Settings saved.'); - } else { - $this->emit('error', 'Instance SMTP settings are not enabled.'); - } - } - - public function sendTestNotification() - { - $this->model->notify(new Test($this->emails)); - $this->emit('success', 'Test Email sent successfully.'); - } - - public function instantSave() - { - try { - $this->submit(); - } catch (\Exception $e) { - $this->model->smtp_enabled = false; - $this->validate(); - } - } - - public function submit() - { - $this->resetErrorBag(); - $this->validate(); - - if ($this->model->smtp_password) { - $this->model->smtp_password = encrypt($this->model->smtp_password); - } else { - $this->model->smtp_password = null; - } - - $this->model->smtp_recipients = str_replace(' ', '', $this->model->smtp_recipients); - $this->saveModel(); - } - - public function saveModel() - { - $this->model->save(); - $this->decrypt(); - if (is_a($this->model, Team::class)) { refreshSession(); + $this->team = $team; + $this->emit('success', 'Settings saved.'); + return; } - $this->emit('success', 'Settings saved.'); + if ($settings->resend_enabled) { + $team = currentTeam(); + $team->update([ + 'resend_enabled' => $settings->resend_enabled, + 'resend_api_key' => $settings->resend_api_key, + ]); + refreshSession(); + $this->team = $team; + $this->emit('success', 'Settings saved.'); + return; + } + $this->emit('error', 'Instance SMTP/Resend settings are not enabled.'); } } diff --git a/app/Http/Livewire/Project/New/GithubPrivateRepository.php b/app/Http/Livewire/Project/New/GithubPrivateRepository.php index 77b2b2dab..dec4e80e8 100644 --- a/app/Http/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Http/Livewire/Project/New/GithubPrivateRepository.php @@ -7,15 +7,19 @@ use App\Models\Project; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; +use App\Traits\SaveFromRedirect; use Illuminate\Support\Facades\Http; use Livewire\Component; +use Route; class GithubPrivateRepository extends Component { + use SaveFromRedirect; public $current_step = 'github_apps'; public $github_apps; public GithubApp $github_app; public $parameters; + public $currentRoute; public $query; public $type; @@ -36,14 +40,30 @@ class GithubPrivateRepository extends Component public string|null $publish_directory = null; protected int $page = 1; + // public function saveFromRedirect(string $route, ?Collection $parameters = null){ + // session()->forget('from'); + // if (!$parameters || $parameters->count() === 0) { + // $parameters = $this->parameters; + // } + // $parameters = collect($parameters) ?? collect([]); + // $queries = collect($this->query) ?? collect([]); + // $parameters = $parameters->merge($queries); + // session(['from'=> [ + // 'back'=> $this->currentRoute, + // 'route' => $route, + // 'parameters' => $parameters + // ]]); + // return redirect()->route($route); + // } + public function mount() { + $this->currentRoute = Route::currentRouteName(); $this->parameters = get_route_parameters(); $this->query = request()->query(); $this->repositories = $this->branches = collect(); $this->github_apps = GithubApp::private(); } - public function loadRepositories($github_app_id) { $this->repositories = collect(); diff --git a/app/Http/Livewire/Project/New/Select.php b/app/Http/Livewire/Project/New/Select.php index b3109ef8c..c6ec91bb3 100644 --- a/app/Http/Livewire/Project/New/Select.php +++ b/app/Http/Livewire/Project/New/Select.php @@ -3,19 +3,28 @@ namespace App\Http\Livewire\Project\New; use App\Models\Server; +use App\Models\StandaloneDocker; +use App\Models\SwarmDocker; use Countable; +use Illuminate\Support\Collection; use Livewire\Component; +use Route; class Select extends Component { public $current_step = 'type'; + public ?int $server = null; public string $type; public string $server_id; public string $destination_uuid; public Countable|array|Server $servers; - public $destinations = []; + public Collection|array $standaloneDockers = []; + public Collection|array $swarmDockers = []; public array $parameters; + protected $queryString = [ + 'server', + ]; public function mount() { $this->parameters = get_route_parameters(); @@ -31,13 +40,20 @@ public function set_type(string $type) $this->set_destination($server->destinations()->first()->uuid); } } + if (!is_null($this->server)) { + $foundServer = $this->servers->where('id', $this->server)->first(); + if ($foundServer) { + return $this->set_server($foundServer); + } + } $this->current_step = 'servers'; } public function set_server(Server $server) { $this->server_id = $server->id; - $this->destinations = $server->destinations(); + $this->standaloneDockers = $server->standaloneDockers; + $this->swarmDockers = $server->swarmDockers; $this->current_step = 'destinations'; } diff --git a/app/Http/Livewire/Server/All.php b/app/Http/Livewire/Server/All.php new file mode 100644 index 000000000..94db86f34 --- /dev/null +++ b/app/Http/Livewire/Server/All.php @@ -0,0 +1,20 @@ +servers = Server::ownedByCurrentTeam()->get(); + } + public function render() + { + return view('livewire.server.all'); + } +} diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php index 290c06dd7..0546c0007 100644 --- a/app/Http/Livewire/Server/Form.php +++ b/app/Http/Livewire/Server/Form.php @@ -4,10 +4,12 @@ use App\Actions\Server\InstallDocker; use App\Models\Server; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class Form extends Component { + use AuthorizesRequests; public Server $server; public $uptime; public $dockerVersion; @@ -64,14 +66,20 @@ public function validateServer() public function delete() { - if (!$this->server->isEmpty()) { - $this->emit('error', 'Server has defined resources. Please delete them first.'); - return; + try { + $this->authorize('delete', $this->server); + if (!$this->server->isEmpty()) { + $this->emit('error', 'Server has defined resources. Please delete them first.'); + return; + } + $this->server->delete(); + return redirect()->route('server.all'); + } catch (\Exception $e) { + return general_error_handler(err: $e, that: $this); } - $this->server->delete(); - redirect()->route('server.all'); - } + + } public function submit() { $this->validate(); diff --git a/app/Http/Livewire/Server/Proxy/Status.php b/app/Http/Livewire/Server/Proxy/Status.php index a4a29b7c2..352a1df52 100644 --- a/app/Http/Livewire/Server/Proxy/Status.php +++ b/app/Http/Livewire/Server/Proxy/Status.php @@ -12,10 +12,12 @@ class Status extends Component public function get_status() { - dispatch_sync(new ProxyContainerStatusJob( - server: $this->server - )); - $this->server->refresh(); - $this->emit('proxyStatusUpdated'); + if (data_get($this->server,'settings.is_usable')) { + dispatch_sync(new ProxyContainerStatusJob( + server: $this->server + )); + $this->server->refresh(); + $this->emit('proxyStatusUpdated'); + } } } diff --git a/app/Http/Livewire/Server/Show.php b/app/Http/Livewire/Server/Show.php new file mode 100644 index 000000000..75053cc1a --- /dev/null +++ b/app/Http/Livewire/Server/Show.php @@ -0,0 +1,25 @@ +server = Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail(); + } catch (\Throwable $e) { + return general_error_handler(err: $e, that: $this); + } + } + public function render() + { + return view('livewire.server.show'); + } +} diff --git a/app/Http/Livewire/Server/PrivateKey.php b/app/Http/Livewire/Server/ShowPrivateKey.php similarity index 96% rename from app/Http/Livewire/Server/PrivateKey.php rename to app/Http/Livewire/Server/ShowPrivateKey.php index 8f433f73d..4f77014cb 100644 --- a/app/Http/Livewire/Server/PrivateKey.php +++ b/app/Http/Livewire/Server/ShowPrivateKey.php @@ -6,7 +6,7 @@ use Livewire\Component; use Masmerise\Toaster\Toaster; -class PrivateKey extends Component +class ShowPrivateKey extends Component { public Server $server; public $privateKeys; diff --git a/app/Http/Livewire/Settings/Email.php b/app/Http/Livewire/Settings/Email.php index a0ac3c904..c0e80f020 100644 --- a/app/Http/Livewire/Settings/Email.php +++ b/app/Http/Livewire/Settings/Email.php @@ -20,6 +20,9 @@ class Email extends Component 'settings.smtp_timeout' => 'nullable', 'settings.smtp_from_address' => 'required|email', 'settings.smtp_from_name' => 'required', + 'settings.resend_enabled' => 'nullable|boolean', + 'settings.resend_api_key' => 'nullable' + ]; protected $validationAttributes = [ 'settings.smtp_from_address' => 'From Address', @@ -30,48 +33,68 @@ class Email extends Component 'settings.smtp_encryption' => 'Encryption', 'settings.smtp_username' => 'Username', 'settings.smtp_password' => 'Password', + 'settings.smtp_timeout' => 'Timeout', + 'settings.resend_api_key' => 'Resend API Key' ]; - public function mount() { - $this->decrypt(); $this->emails = auth()->user()->email; } - private function decrypt() - { - if (data_get($this->settings, 'smtp_password')) { - try { - $this->settings->smtp_password = decrypt($this->settings->smtp_password); - } catch (\Exception $e) { - } + public function submitFromFields() { + try { + $this->resetErrorBag(); + $this->validate([ + 'settings.smtp_from_address' => 'required|email', + 'settings.smtp_from_name' => 'required', + ]); + $this->settings->save(); + $this->emit('success', 'Settings saved successfully.'); + } catch (\Exception $e) { + return general_error_handler($e, $this); + } + } + public function submitResend() { + try { + $this->resetErrorBag(); + $this->validate([ + 'settings.resend_api_key' => 'required' + ]); + $this->settings->smtp_enabled = false; + $this->settings->save(); + $this->emit('success', 'Settings saved successfully.'); + } catch (\Exception $e) { + $this->settings->resend_enabled = false; + return general_error_handler($e, $this); } } - public function instantSave() { try { $this->submit(); - $this->emit('success', 'Settings saved successfully.'); } catch (\Exception $e) { - $this->settings->smtp_enabled = false; - $this->validate(); + return general_error_handler($e, $this); } } public function submit() { - $this->resetErrorBag(); - $this->validate(); - if ($this->settings->smtp_password) { - $this->settings->smtp_password = encrypt($this->settings->smtp_password); - } else { - $this->settings->smtp_password = null; + try { + $this->resetErrorBag(); + $this->validate([ + 'settings.smtp_host' => 'required', + 'settings.smtp_port' => 'required|numeric', + 'settings.smtp_encryption' => 'nullable', + 'settings.smtp_username' => 'nullable', + 'settings.smtp_password' => 'nullable', + 'settings.smtp_timeout' => 'nullable', + ]); + $this->settings->resend_enabled = false; + $this->settings->save(); + $this->emit('success', 'Settings saved successfully.'); + } catch (\Exception $e) { + return general_error_handler($e, $this); } - - $this->settings->save(); - $this->emit('success', 'Transaction email settings updated successfully.'); - $this->decrypt(); } public function sendTestNotification() diff --git a/app/Http/Livewire/Source/Github/Change.php b/app/Http/Livewire/Source/Github/Change.php index e2be4e0ab..32f84e8ed 100644 --- a/app/Http/Livewire/Source/Github/Change.php +++ b/app/Http/Livewire/Source/Github/Change.php @@ -37,9 +37,13 @@ class Change extends Component public function mount() { - $this->webhook_endpoint = $this->ipv4; + if (isCloud() && !isDev()) { + $this->webhook_endpoint = config('app.url'); + } else { + $this->webhook_endpoint = $this->ipv4; + $this->is_system_wide = $this->github_app->is_system_wide; + } $this->parameters = get_route_parameters(); - $this->is_system_wide = $this->github_app->is_system_wide; } public function submit() diff --git a/app/Http/Livewire/Source/Github/Create.php b/app/Http/Livewire/Source/Github/Create.php index 4d6f8b26b..b5487bcc8 100644 --- a/app/Http/Livewire/Source/Github/Create.php +++ b/app/Http/Livewire/Source/Github/Create.php @@ -42,6 +42,9 @@ public function createGitHubApp() 'is_system_wide' => $this->is_system_wide, 'team_id' => currentTeam()->id, ]); + if (session('from')) { + session(['from' => session('from') + ['source_id' => $github_app->id]]); + } redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]); } catch (\Exception $e) { return general_error_handler(err: $e, that: $this); diff --git a/app/Http/Livewire/Subscription/PricingPlans.php b/app/Http/Livewire/Subscription/PricingPlans.php index fcf24237f..7d77f68c4 100644 --- a/app/Http/Livewire/Subscription/PricingPlans.php +++ b/app/Http/Livewire/Subscription/PricingPlans.php @@ -48,8 +48,8 @@ public function subscribeStripe($type) 'enabled' => true, ], 'mode' => 'subscription', - 'success_url' => route('subscription.success'), - 'cancel_url' => route('subscription.show',['cancelled' => true]), + 'success_url' => route('dashboard', ['success' => true]), + 'cancel_url' => route('subscription.index', ['cancelled' => true]), ]; $customer = currentTeam()->subscription?->stripe_customer_id ?? null; if ($customer) { diff --git a/app/Http/Livewire/Team/Create.php b/app/Http/Livewire/Team/Create.php index bd2c24313..0e721814c 100644 --- a/app/Http/Livewire/Team/Create.php +++ b/app/Http/Livewire/Team/Create.php @@ -30,7 +30,7 @@ public function submit() ]); auth()->user()->teams()->attach($team, ['role' => 'admin']); refreshSession(); - return redirect()->route('team.show'); + return redirect()->route('team.index'); } catch (\Throwable $th) { return general_error_handler($th, $this); } diff --git a/app/Http/Livewire/Team/Delete.php b/app/Http/Livewire/Team/Delete.php index 5e206704b..32e386ebc 100644 --- a/app/Http/Livewire/Team/Delete.php +++ b/app/Http/Livewire/Team/Delete.php @@ -25,6 +25,6 @@ public function delete() }); refreshSession(); - return redirect()->route('team.show'); + return redirect()->route('team.index'); } } diff --git a/app/Http/Livewire/Team/Form.php b/app/Http/Livewire/Team/Form.php index caa6d2c9e..16aff27ee 100644 --- a/app/Http/Livewire/Team/Form.php +++ b/app/Http/Livewire/Team/Form.php @@ -28,7 +28,6 @@ public function submit() try { $this->team->save(); refreshSession(); - $this->emit('reloadWindow'); } catch (\Throwable $th) { return general_error_handler($th, $this); } diff --git a/app/Http/Livewire/Team/Invitations.php b/app/Http/Livewire/Team/Invitations.php index ba6c1e91f..f52701e34 100644 --- a/app/Http/Livewire/Team/Invitations.php +++ b/app/Http/Livewire/Team/Invitations.php @@ -14,6 +14,7 @@ public function deleteInvitation(int $invitation_id) { TeamInvitation::find($invitation_id)->delete(); $this->refreshInvitations(); + $this->emit('success', 'Invitation revoked.'); } public function refreshInvitations() diff --git a/app/Http/Livewire/Waitlist.php b/app/Http/Livewire/Waitlist/Index.php similarity index 77% rename from app/Http/Livewire/Waitlist.php rename to app/Http/Livewire/Waitlist/Index.php index d9b3b92ab..06d224afe 100644 --- a/app/Http/Livewire/Waitlist.php +++ b/app/Http/Livewire/Waitlist/Index.php @@ -1,22 +1,27 @@ 'required|email', ]; + public function render() + { + return view('livewire.waitlist.index')->layout('layouts.simple'); + } public function mount() { + $this->waitingInLine = Waitlist::whereVerified(true)->count(); if (isDev()) { $this->email = 'waitlist@example.com'; } @@ -29,7 +34,7 @@ public function submit() if ($already_registered) { throw new \Exception('You are already on the waitlist or registered.
Please check your email to verify your email address or contact support.'); } - $found = ModelsWaitlist::where('email', $this->email)->first(); + $found = Waitlist::where('email', $this->email)->first(); if ($found) { if (!$found->verified) { $this->emit('error', 'You are already on the waitlist.
Please check your email to verify your email address.'); @@ -38,7 +43,7 @@ public function submit() $this->emit('error', 'You are already on the waitlist.
You will be notified when your turn comes.
Thank you.'); return; } - $waitlist = ModelsWaitlist::create([ + $waitlist = Waitlist::create([ 'email' => $this->email, 'type' => 'registration', ]); diff --git a/app/Http/Middleware/IsBoardingFlow.php b/app/Http/Middleware/IsBoardingFlow.php index 83662a073..e0542a57a 100644 --- a/app/Http/Middleware/IsBoardingFlow.php +++ b/app/Http/Middleware/IsBoardingFlow.php @@ -15,7 +15,7 @@ class IsBoardingFlow */ public function handle(Request $request, Closure $next): Response { - // ray('IsBoardingFlow Middleware'); + ray()->showQueries()->color('orange'); if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) { return redirect('boarding'); } diff --git a/app/Http/Middleware/SubscriptionValid.php b/app/Http/Middleware/IsSubscriptionValid.php similarity index 94% rename from app/Http/Middleware/SubscriptionValid.php rename to app/Http/Middleware/IsSubscriptionValid.php index 824133f5a..a9354e982 100644 --- a/app/Http/Middleware/SubscriptionValid.php +++ b/app/Http/Middleware/IsSubscriptionValid.php @@ -6,14 +6,14 @@ use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; -class SubscriptionValid +class IsSubscriptionValid { public function handle(Request $request, Closure $next): Response { if (isInstanceAdmin()) { return $next($request); } - if (!auth()->user() || !is_cloud()) { + if (!auth()->user() || !isCloud()) { if ($request->path() === 'subscription') { return redirect('/'); } else { diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 5f24d4ab8..715b22b91 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -20,6 +20,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -65,6 +66,12 @@ class ApplicationDeploymentJob implements ShouldQueue private $log_model; private Collection $saved_outputs; + public function middleware(): array + { + return [ + (new WithoutOverlapping("dockerimagejobs"))->shared(), + ]; + } public function __construct(int $application_deployment_queue_id) { ray()->clearScreen(); diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php index b133279ea..1275839d9 100644 --- a/app/Jobs/CleanupInstanceStuffsJob.php +++ b/app/Jobs/CleanupInstanceStuffsJob.php @@ -37,7 +37,7 @@ public function handle(): void private function cleanup_waitlist() { - $waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.confirmation_valid_for_minutes')))->get(); + $waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.expiration')))->get(); foreach ($waitlist as $item) { $item->delete(); } diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 7a8571c00..4456eee5e 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -7,6 +7,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Str; @@ -15,51 +16,69 @@ class DockerCleanupJob implements ShouldQueue use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public $timeout = 500; + public ?string $dockerRootFilesystem = null; + public ?int $usageBefore = null; - /** - * Create a new job instance. - */ + public function middleware(): array + { + return [ + (new WithoutOverlapping("dockerimagejobs"))->shared(), + ]; + } public function __construct() { - // } - - /** - * Execute the job. - */ public function handle(): void { try { + ray()->showQueries()->color('orange'); $servers = Server::all(); foreach ($servers as $server) { - if (isDev()) { - $docker_root_filesystem = "/"; - } else { - $docker_root_filesystem = instant_remote_process(['stat --printf=%m $(docker info --format "{{json .DockerRootDir}}" |sed \'s/"//g\')'], $server); + if ( + !$server->settings->is_reachable && !$server->settings->is_usable + ) { + continue; } - $disk_percentage_before = $this->get_disk_usage($server, $docker_root_filesystem); - if ($disk_percentage_before >= $server->settings->cleanup_after_percentage) { + if (isDev()) { + $this->dockerRootFilesystem = "/"; + } else { + $this->dockerRootFilesystem = instant_remote_process( + [ + "stat --printf=%m $(docker info --format '{{json .DockerRootDir}}'' |sed 's/\"//g')" + ], + $server, + false + ); + } + if (!$this->dockerRootFilesystem) { + continue; + } + $this->usageBefore = $this->getFilesystemUsage($server); + if ($this->usageBefore >= $server->settings->cleanup_after_percentage) { + ray('Cleaning up ' . $server->name)->color('orange'); instant_remote_process(['docker image prune -af'], $server); instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server); instant_remote_process(['docker builder prune -af'], $server); - $disk_percentage_after = $this->get_disk_usage($server, $docker_root_filesystem); - if ($disk_percentage_after < $disk_percentage_before) { - ray('Saved ' . ($disk_percentage_before - $disk_percentage_after) . '% disk space on ' . $server->name); + $usageAfter = $this->getFilesystemUsage($server); + if ($usageAfter < $this->usageBefore) { + ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $server->name)->color('orange'); + send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $server->name); + } else { + ray('DockerCleanupJob failed to save disk space on ' . $server->name)->color('orange'); } + } else { + ray('No need to clean up ' . $server->name)->color('orange'); } } } catch (\Exception $e) { send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage()); - ray($e->getMessage()); + ray($e->getMessage())->color('orange'); throw $e; } } - private function get_disk_usage(Server $server, string $docker_root_filesystem) + private function getFilesystemUsage(Server $server) { - $disk_usage = json_decode(instant_remote_process(['df -hP | awk \'BEGIN {printf"{\\"disks\\":["}{if($1=="Filesystem")next;if(a)printf",";printf"{\\"mount\\":\\""$6"\\",\\"size\\":\\""$2"\\",\\"used\\":\\""$3"\\",\\"avail\\":\\""$4"\\",\\"use%\\":\\""$5"\\"}";a++;}END{print"]}";}\''], $server), true); - $mount_point = collect(data_get($disk_usage, 'disks'))->where('mount', $docker_root_filesystem)->first(); - ray($mount_point); - return Str::of(data_get($mount_point, 'use%'))->trim()->replace('%', '')->value(); + return instant_remote_process(["df '{$this->dockerRootFilesystem}'| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $server, false); } } diff --git a/app/Jobs/ProxyCheckJob.php b/app/Jobs/ProxyCheckJob.php index 9d5559053..720dd2ea1 100755 --- a/app/Jobs/ProxyCheckJob.php +++ b/app/Jobs/ProxyCheckJob.php @@ -33,8 +33,9 @@ public function handle() if ($status === 'running') { continue; } - // $server->team->notify(new ProxyStoppedNotification($server)); - resolve(StartProxy::class)($server); + if (data_get($server, 'proxy.type')) { + resolve(StartProxy::class)($server); + } } } catch (\Throwable $th) { ray($th->getMessage()); diff --git a/app/Jobs/ProxyContainerStatusJob.php b/app/Jobs/ProxyContainerStatusJob.php index aa6056434..24ad24be0 100644 --- a/app/Jobs/ProxyContainerStatusJob.php +++ b/app/Jobs/ProxyContainerStatusJob.php @@ -39,9 +39,9 @@ public function uniqueId(): int public function handle(): void { try { - $container = getContainerStatus(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: true); + $container = getContainerStatus(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: false); $status = data_get($container, 'State.Status'); - if (data_get($this->server,'proxy.status') !== $status) { + if ($status && data_get($this->server, 'proxy.status') !== $status) { $this->server->proxy->status = $status; if ($this->server->proxy->status === 'running') { $traefik = $container['Config']['Labels']['org.opencontainers.image.title']; diff --git a/app/Jobs/ProxyStartJob.php b/app/Jobs/ProxyStartJob.php index 4e2331a68..91932a48c 100755 --- a/app/Jobs/ProxyStartJob.php +++ b/app/Jobs/ProxyStartJob.php @@ -3,6 +3,8 @@ namespace App\Jobs; use App\Actions\Proxy\StartProxy; +use App\Enums\ProxyStatus; +use App\Enums\ProxyTypes; use App\Models\Server; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -27,6 +29,11 @@ public function handle() if ($status === 'running') { return; } + if (is_null(data_get($this->server, 'proxy.type'))) { + $this->server->proxy->type = ProxyTypes::TRAEFIK_V2->value; + $this->server->proxy->status = ProxyStatus::EXITED->value; + $this->server->save(); + } resolve(StartProxy::class)($this->server); } catch (\Throwable $th) { send_internal_notification('ProxyStartJob failed with: ' . $th->getMessage()); diff --git a/app/Jobs/SendConfirmationForWaitlistJob.php b/app/Jobs/SendConfirmationForWaitlistJob.php index 3ff4982d9..041b418ec 100755 --- a/app/Jobs/SendConfirmationForWaitlistJob.php +++ b/app/Jobs/SendConfirmationForWaitlistJob.php @@ -37,7 +37,7 @@ public function handle() $mail->subject('You are on the waitlist!'); send_user_an_email($mail, $this->email); } catch (\Throwable $th) { - send_internal_notification('SendConfirmationForWaitlistJob failed with error: ' . $th->getMessage()); + send_internal_notification("SendConfirmationForWaitlistJob failed for {$mail} with error: " . $th->getMessage()); ray($th->getMessage()); throw $th; } diff --git a/app/Jobs/SubscriptionInvoiceFailedJob.php b/app/Jobs/SubscriptionInvoiceFailedJob.php index d6750f953..48e9fea64 100755 --- a/app/Jobs/SubscriptionInvoiceFailedJob.php +++ b/app/Jobs/SubscriptionInvoiceFailedJob.php @@ -9,7 +9,6 @@ use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Stripe\Stripe; class SubscriptionInvoiceFailedJob implements ShouldQueue { diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index 0fc892d92..057595351 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -13,6 +13,7 @@ class InstanceSettings extends Model implements SendsEmail protected $guarded = []; protected $casts = [ 'resale_license' => 'encrypted', + 'smtp_password' => 'encrypted', ]; public static function get() diff --git a/app/Models/Server.php b/app/Models/Server.php index 764c58246..f3d9090a0 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -33,6 +33,9 @@ protected static function booted() }); static::deleting(function ($server) { + $server->destinations()->each(function ($destination) { + $destination->delete(); + }); $server->settings()->delete(); }); } @@ -70,8 +73,6 @@ static public function destinationsByServer(string $server_id) return $standaloneDocker->concat($swarmDocker); } - - public function settings() { return $this->hasOne(ServerSetting::class); @@ -84,12 +85,20 @@ public function scopeWithProxy(): Builder public function isEmpty() { - if ($this->applications()->count() === 0) { + $applications = $this->applications()->count() === 0; + $databases = $this->databases()->count() === 0; + if ($applications && $databases) { return true; } return false; } + public function databases() { + return $this->destinations()->map(function ($standaloneDocker) { + $postgresqls = $standaloneDocker->postgresqls; + return $postgresqls?->concat([]) ?? collect([]); + })->flatten(); + } public function applications() { return $this->destinations()->map(function ($standaloneDocker) { diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index e0a52d5b1..4acd7fe8a 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Str; class Subscription extends Model { @@ -14,19 +15,44 @@ public function team() } public function type() { - $basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids')); - $pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids')); - $ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids')); + if (isLemon()) { + $basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids')); + $pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids')); + $ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids')); - $subscription = $this->lemon_variant_id; - if (in_array($subscription, $basic)) { - return 'basic'; + $subscription = $this->lemon_variant_id; + if (in_array($subscription, $basic)) { + return 'basic'; + } + if (in_array($subscription, $pro)) { + return 'pro'; + } + if (in_array($subscription, $ultimate)) { + return 'ultimate'; + } } - if (in_array($subscription, $pro)) { - return 'pro'; - } - if (in_array($subscription, $ultimate)) { - return 'ultimate'; + if (isStripe()) { + if (!$this->stripe_plan_id) { + return 'unknown'; + } + $subscription = Subscription::where('id', $this->id)->first(); + if (!$subscription) { + return null; + } + $subscriptionPlanId = data_get($subscription,'stripe_plan_id'); + if (!$subscriptionPlanId) { + return null; + } + $subscriptionConfigs = collect(config('subscription')); + $stripePlanId = null; + $subscriptionConfigs->map(function ($value, $key) use ($subscriptionPlanId, &$stripePlanId) { + if ($value === $subscriptionPlanId){ + $stripePlanId = $key; + }; + })->first(); + if ($stripePlanId) { + return Str::of($stripePlanId)->after('stripe_price_id_')->before('_')->lower(); + } } return 'unknown'; } diff --git a/app/Models/Team.php b/app/Models/Team.php index d8d486fec..b75772569 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -4,6 +4,7 @@ use App\Notifications\Channels\SendsDiscord; use App\Notifications\Channels\SendsEmail; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; @@ -14,6 +15,8 @@ class Team extends Model implements SendsDiscord, SendsEmail protected $guarded = []; protected $casts = [ 'personal_team' => 'boolean', + 'smtp_password' => 'encrypted', + 'resend_api_key' => 'encrypted', ]; public function routeNotificationForDiscord() @@ -30,6 +33,27 @@ public function getRecepients($notification) } return explode(',', $recipients); } + public function limits(): Attribute + { + return Attribute::make( + get: function () { + if (config('coolify.self_hosted') || $this->id === 0) { + $subscription = 'self-hosted'; + } else { + $subscription = data_get($this, 'subscription'); + if (is_null($subscription)) { + $subscription = 'zero'; + } else { + $subscription = $subscription->type(); + } + } + $serverLimit = config('constants.limits.server')[strtolower($subscription)]; + $sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)]; + return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled]; + } + + ); + } public function members() { diff --git a/app/Models/User.php b/app/Models/User.php index 13140f133..9050de214 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -32,6 +32,7 @@ protected static function boot() $team = [ 'name' => $user->name . "'s Team", 'personal_team' => true, + 'show_boarding' => true ]; if ($user->id === 0) { $team['id'] = 0; @@ -91,29 +92,20 @@ public function isInstanceAdmin() return $found_root_team->count() > 0; } - public function personalTeam() - { - return $this->teams()->where('personal_team', true)->first(); - } - public function currentTeam() { - return $this->teams()->where('team_id', session('currentTeam')->id)->first(); + return Team::find(session('currentTeam')->id); } public function otherTeams() { - $team_id = currentTeam()->id; - return auth()->user()->teams->filter(function ($team) use ($team_id) { - return $team->id != $team_id; + return auth()->user()->teams->filter(function ($team) { + return $team->id != currentTeam()->id; }); } public function role() { - if ($this->teams()->where('team_id', 0)->first()) { - return 'admin'; - } - return $this->teams()->where('team_id', currentTeam()->id)->first()->pivot->role; + return session('currentTeam')->pivot->role; } } diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index bc7f5dfde..fdd4beabf 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -44,7 +44,7 @@ public function __construct(Application $application, string $deployment_uuid, A public function via(object $notifiable): array { $channels = []; - $isEmailEnabled = data_get($notifiable, 'smtp_enabled'); + $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_deployments'); $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_deployments'); diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index b412bebd6..aaf059149 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -6,10 +6,10 @@ use Illuminate\Mail\Message; use Illuminate\Notifications\Notification; use Illuminate\Support\Facades\Mail; -use Illuminate\Support\Str; class EmailChannel { + private bool $isResend = false; public function send(SendsEmail $notifiable, Notification $notification): void { $this->bootConfigs($notifiable); @@ -20,29 +20,55 @@ public function send(SendsEmail $notifiable, Notification $notification): void } $mailMessage = $notification->toMail($notifiable); - Mail::send( - [], - [], - fn (Message $message) => $message - ->from( - data_get($notifiable, 'smtp_from_address'), - data_get($notifiable, 'smtp_from_name'), - ) - ->bcc($recepients) - ->subject($mailMessage->subject) - ->html((string)$mailMessage->render()) - ); + if ($this->isResend) { + foreach ($recepients as $receipient) { + Mail::send( + [], + [], + fn (Message $message) => $message + ->from( + data_get($notifiable, 'smtp_from_address'), + data_get($notifiable, 'smtp_from_name'), + ) + ->to($receipient) + ->subject($mailMessage->subject) + ->html((string)$mailMessage->render()) + ); + } + } else { + Mail::send( + [], + [], + fn (Message $message) => $message + ->from( + data_get($notifiable, 'smtp_from_address'), + data_get($notifiable, 'smtp_from_name'), + ) + ->bcc($recepients) + ->subject($mailMessage->subject) + ->html((string)$mailMessage->render()) + ); + } } private function bootConfigs($notifiable): void { - $password = data_get($notifiable, 'smtp_password'); - if ($password) $password = decrypt($password); - - if (Str::contains(data_get($notifiable, 'smtp_host'),'resend.com')) { + if (data_get($notifiable, 'use_instance_email_settings')) { + $type = set_transanctional_email_settings(); + if (!$type) { + throw new Exception('No email settings found.'); + } + if ($type === 'resend') { + $this->isResend = true; + } + return; + } + if (data_get($notifiable, 'resend_enabled')) { + $this->isResend = true; config()->set('mail.default', 'resend'); - config()->set('resend.api_key', $password); - } else { + config()->set('resend.api_key', data_get($notifiable, 'resend_api_key')); + } + if (data_get($notifiable, 'smtp_enabled')) { config()->set('mail.default', 'smtp'); config()->set('mail.mailers.smtp', [ "transport" => "smtp", @@ -50,7 +76,7 @@ private function bootConfigs($notifiable): void "port" => data_get($notifiable, 'smtp_port'), "encryption" => data_get($notifiable, 'smtp_encryption'), "username" => data_get($notifiable, 'smtp_username'), - "password" => $password, + "password" => data_get($notifiable, 'smtp_password'), "timeout" => data_get($notifiable, 'smtp_timeout'), "local_domain" => null, ]); diff --git a/app/Notifications/Channels/TransactionalEmailChannel.php b/app/Notifications/Channels/TransactionalEmailChannel.php index bf968eb3f..23fe28700 100644 --- a/app/Notifications/Channels/TransactionalEmailChannel.php +++ b/app/Notifications/Channels/TransactionalEmailChannel.php @@ -4,16 +4,20 @@ use App\Models\InstanceSettings; use App\Models\User; +use Exception; use Illuminate\Mail\Message; use Illuminate\Notifications\Notification; use Illuminate\Support\Facades\Mail; +use Log; class TransactionalEmailChannel { + private bool $isResend = false; public function send(User $notifiable, Notification $notification): void { $settings = InstanceSettings::get(); - if (data_get($settings, 'smtp_enabled') !== true) { + if (!data_get($settings, 'smtp_enabled') && !data_get($settings, 'resend_enabled')) { + Log::info('SMTP/Resend not enabled'); return; } $email = $notifiable->email; @@ -22,22 +26,43 @@ public function send(User $notifiable, Notification $notification): void } $this->bootConfigs(); $mailMessage = $notification->toMail($notifiable); - Mail::send( - [], - [], - fn (Message $message) => $message - ->from( - data_get($settings, 'smtp_from_address'), - data_get($settings, 'smtp_from_name') - ) - ->to($email) - ->subject($mailMessage->subject) - ->html((string)$mailMessage->render()) - ); + if ($this->isResend) { + Mail::send( + [], + [], + fn (Message $message) => $message + ->from( + data_get($settings, 'smtp_from_address'), + data_get($settings, 'smtp_from_name'), + ) + ->to($email) + ->subject($mailMessage->subject) + ->html((string)$mailMessage->render()) + ); + } else { + Mail::send( + [], + [], + fn (Message $message) => $message + ->from( + data_get($settings, 'smtp_from_address'), + data_get($settings, 'smtp_from_name'), + ) + ->bcc($email) + ->subject($mailMessage->subject) + ->html((string)$mailMessage->render()) + ); + } } private function bootConfigs(): void { - set_transanctional_email_settings(); + $type = set_transanctional_email_settings(); + if (!$type) { + throw new Exception('No email settings found.'); + } + if ($type === 'resend') { + $this->isResend = true; + } } } diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php index b47f03291..dda0b4884 100644 --- a/app/Notifications/Database/BackupFailed.php +++ b/app/Notifications/Database/BackupFailed.php @@ -25,7 +25,7 @@ public function __construct(ScheduledDatabaseBackup $backup, public $database, p public function via(object $notifiable): array { $channels = []; - $isEmailEnabled = data_get($notifiable, 'smtp_enabled'); + $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups'); $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups'); diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index 82b521019..dad3ee060 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -25,7 +25,7 @@ public function __construct(ScheduledDatabaseBackup $backup, public $database) public function via(object $notifiable): array { $channels = []; - $isEmailEnabled = data_get($notifiable, 'smtp_enabled'); + $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups'); $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups'); diff --git a/app/Notifications/Server/NotReachable.php b/app/Notifications/Server/NotReachable.php index 0b5f71569..bc97d033e 100644 --- a/app/Notifications/Server/NotReachable.php +++ b/app/Notifications/Server/NotReachable.php @@ -23,7 +23,7 @@ public function __construct(public Server $server) public function via(object $notifiable): array { $channels = []; - $isEmailEnabled = data_get($notifiable, 'smtp_enabled'); + $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_status_changes'); $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_status_changes'); diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index 4cec28cfd..eb266d0f9 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -20,7 +20,7 @@ public function __construct(public string|null $emails = null) public function via(object $notifiable): array { $channels = []; - $isEmailEnabled = data_get($notifiable, 'smtp_enabled'); + $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); if ($isDiscordEnabled && empty($this->emails)) { diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php index e9baa16d1..6844aa705 100644 --- a/app/Notifications/TransactionalEmails/ResetPassword.php +++ b/app/Notifications/TransactionalEmails/ResetPassword.php @@ -31,24 +31,11 @@ public static function toMailUsing($callback) public function via($notifiable) { - if ($this->settings->smtp_enabled) { - $password = data_get($this->settings, 'smtp_password'); - if ($password) $password = decrypt($password); - - config()->set('mail.default', 'smtp'); - config()->set('mail.mailers.smtp', [ - "transport" => "smtp", - "host" => data_get($this->settings, 'smtp_host'), - "port" => data_get($this->settings, 'smtp_port'), - "encryption" => data_get($this->settings, 'smtp_encryption'), - "username" => data_get($this->settings, 'smtp_username'), - "password" => $password, - "timeout" => data_get($this->settings, 'smtp_timeout'), - "local_domain" => null, - ]); - return ['mail']; + $type = set_transanctional_email_settings(); + if (!$type) { + throw new \Exception('No email settings found.'); } - throw new \Exception('SMTP is not enabled'); + return ['mail']; } public function toMail($notifiable) diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php new file mode 100644 index 000000000..08ee5e64d --- /dev/null +++ b/app/Policies/ServerPolicy.php @@ -0,0 +1,66 @@ +teams()->get()->firstWhere('id', $server->team_id) !== null; + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return true; + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Server $server): bool + { + return $user->teams()->get()->firstWhere('id', $server->team_id) !== null; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Server $server): bool + { + return $user->teams()->get()->firstWhere('id', $server->team_id) !== null; + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Server $server): bool + { + return false; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Server $server): bool + { + return false; + } +} diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index eee442390..a5fce4858 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -43,22 +43,16 @@ public function toResponse($request) */ public function boot(): void { - Fortify::createUsersUsing(CreateNewUser::class); Fortify::registerView(function () { $settings = InstanceSettings::get(); - $waiting_in_line = Waitlist::whereVerified(true)->count(); if (!$settings->is_registration_enabled) { return redirect()->route('login'); } if (config('coolify.waitlist')) { - return view('auth.waitlist',[ - 'waiting_in_line' => $waiting_in_line, - ]); + return redirect()->route('waitlist.index'); } else { - return view('auth.register',[ - 'waiting_in_line' => $waiting_in_line, - ]); + return view('auth.register'); } }); diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 24e17a73d..fd20c3764 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -41,6 +41,7 @@ public function execute_remote_command(...$commands) $remote_command = generate_ssh_command($private_key_location, $ip, $user, $port, $command); $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) { + $output = Str::of($output)->trim(); $new_log_entry = [ 'command' => $command, 'output' => $output, diff --git a/app/Traits/SaveFromRedirect.php b/app/Traits/SaveFromRedirect.php new file mode 100644 index 000000000..83013a857 --- /dev/null +++ b/app/Traits/SaveFromRedirect.php @@ -0,0 +1,25 @@ +forget('from'); + if (!$parameters || $parameters->count() === 0) { + $parameters = $this->parameters; + } + $parameters = collect($parameters) ?? collect([]); + $queries = collect($this->query) ?? collect([]); + $parameters = $parameters->merge($queries); + session(['from' => [ + 'back' => $this->currentRoute, + 'route' => $route, + 'parameters' => $parameters + ]]); + return redirect()->route($route); + } +} diff --git a/app/View/Components/Forms/Button.php b/app/View/Components/Forms/Button.php index 2b67c6915..c9f4ebdc0 100644 --- a/app/View/Components/Forms/Button.php +++ b/app/View/Components/Forms/Button.php @@ -15,7 +15,7 @@ public function __construct( public bool $disabled = false, public bool $isModal = false, public bool $noStyle = false, - public string|null $modalId = null, + public ?string $modalId = null, public string $defaultClass = "btn btn-primary btn-sm font-normal text-white normal-case no-animation rounded border-none" ) { if ($this->noStyle) { @@ -23,9 +23,6 @@ public function __construct( } } - /** - * Get the view / contents that represent the component. - */ public function render(): View|Closure|string { return view('components.forms.button'); diff --git a/app/View/Components/Forms/Checkbox.php b/app/View/Components/Forms/Checkbox.php index 1f1d634f3..4fd05c2dd 100644 --- a/app/View/Components/Forms/Checkbox.php +++ b/app/View/Components/Forms/Checkbox.php @@ -17,7 +17,7 @@ public function __construct( public string|null $value = null, public string|null $label = null, public string|null $helper = null, - public bool $instantSave = false, + public string|bool $instantSave = false, public bool $disabled = false, public string $defaultClass = "toggle toggle-xs toggle-warning rounded disabled:bg-coolgray-200 disabled:opacity-50 placeholder:text-neutral-700" ) { diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 8ee0765a5..1094fb06f 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -58,9 +58,11 @@ function format_docker_envs_to_json($rawOutput) } function getApplicationContainerStatus(Application $application) { - $server = $application->destination->server; + $server = data_get($application,'destination.server'); $id = $application->id; - + if (!$server) { + return 'exited'; + } $containers = getCurrentApplicationContainerStatus($server, $id); if ($containers->count() > 0) { $status = data_get($containers[0], 'State', 'exited'); diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 86e11bcf3..b99dc3a9d 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -16,11 +16,6 @@ use Illuminate\Support\Sleep; use Spatie\Activitylog\Models\Activity; -/** - * Run a Remote Process, which SSH's asynchronously into a machine to run the command(s). - * @TODO Change 'root' to 'coolify' when it's able to run Docker commands without sudo - * - */ function remote_process( array $command, Server $server, @@ -167,17 +162,23 @@ function validateServer(Server $server) { try { refresh_server_connection($server->privateKey); - $uptime = instant_remote_process(['uptime'], $server); + $uptime = instant_remote_process(['uptime'], $server, false); if (!$uptime) { - $uptime = 'Server not reachable.'; - throw new \Exception('Server not reachable.'); + $server->settings->is_reachable = false; + return [ + "uptime" => null, + "dockerVersion" => null, + ]; } $server->settings->is_reachable = true; $dockerVersion = instant_remote_process(['docker version|head -2|grep -i version'], $server, false); if (!$dockerVersion) { - $dockerVersion = 'Not installed.'; - throw new \Exception('Docker not installed.'); + $dockerVersion = null; + return [ + "uptime" => $uptime, + "dockerVersion" => null, + ]; } $server->settings->is_usable = true; return [ diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index f1670723e..249f76920 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -7,6 +7,7 @@ use Illuminate\Database\QueryException; use Illuminate\Mail\Message; use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Route; @@ -52,12 +53,13 @@ function showBoarding(): bool } function refreshSession(): void { - $team = currentTeam(); + $team = Team::find(currentTeam()->id); session(['currentTeam' => $team]); } function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed { try { + ray($err); ray('ERROR OCCURRED: ' . $err->getMessage()); if ($err instanceof QueryException) { if ($err->errorInfo[0] === '23505') { @@ -70,6 +72,9 @@ function general_error_handler(Throwable | null $err = null, $that = null, $isJs } elseif ($err instanceof TooManyRequestsException) { throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds."); } else { + if ($err->getMessage() === 'This action is unauthorized.') { + return redirect()->route('dashboard')->with('error', $customErrorMessage ?? $err->getMessage()); + } throw new Exception($customErrorMessage ?? $err->getMessage()); } } catch (Throwable $error) { @@ -117,10 +122,11 @@ function generateSSHKey() $key = RSA::createKey(); return [ 'private' => $key->toString('PKCS1'), - 'public' => $key->getPublicKey()->toString('OpenSSH',['comment' => 'coolify-generated-ssh-key']) + 'public' => $key->getPublicKey()->toString('OpenSSH', ['comment' => 'coolify-generated-ssh-key']) ]; } -function formatPrivateKey(string $privateKey) { +function formatPrivateKey(string $privateKey) +{ $privateKey = trim($privateKey); if (!str_ends_with($privateKey, "\n")) { $privateKey .= "\n"; @@ -135,30 +141,34 @@ function generate_application_name(string $git_repository, string $git_branch): function is_transactional_emails_active(): bool { - return data_get(InstanceSettings::get(), 'smtp_enabled'); + return isEmailEnabled(InstanceSettings::get()); } -function set_transanctional_email_settings(InstanceSettings | null $settings = null): void +function set_transanctional_email_settings(InstanceSettings | null $settings = null): string|null { if (!$settings) { $settings = InstanceSettings::get(); } - $password = data_get($settings, 'smtp_password'); - if (isset($password)) { - $password = decrypt($password); + if (data_get($settings, 'resend_enabled')) { + config()->set('mail.default', 'resend'); + config()->set('resend.api_key', data_get($settings, 'resend_api_key')); + return 'resend'; } - - config()->set('mail.default', 'smtp'); - config()->set('mail.mailers.smtp', [ - "transport" => "smtp", - "host" => data_get($settings, 'smtp_host'), - "port" => data_get($settings, 'smtp_port'), - "encryption" => data_get($settings, 'smtp_encryption'), - "username" => data_get($settings, 'smtp_username'), - "password" => $password, - "timeout" => data_get($settings, 'smtp_timeout'), - "local_domain" => null, - ]); + if (data_get($settings, 'smtp_enabled')) { + config()->set('mail.default', 'smtp'); + config()->set('mail.mailers.smtp', [ + "transport" => "smtp", + "host" => data_get($settings, 'smtp_host'), + "port" => data_get($settings, 'smtp_port'), + "encryption" => data_get($settings, 'smtp_encryption'), + "username" => data_get($settings, 'smtp_username'), + "password" => data_get($settings, 'smtp_password'), + "timeout" => data_get($settings, 'smtp_timeout'), + "local_domain" => null, + ]); + return 'smtp'; + } + return null; } function base_ip(): string @@ -212,7 +222,7 @@ function isDev(): bool return config('app.env') === 'local'; } -function is_cloud(): bool +function isCloud(): bool { return !config('coolify.self_hosted'); } @@ -241,7 +251,10 @@ function send_internal_notification(string $message): void function send_user_an_email(MailMessage $mail, string $email): void { $settings = InstanceSettings::get(); - set_transanctional_email_settings($settings); + $type = set_transanctional_email_settings($settings); + if (!$type) { + throw new Exception('No email settings found.'); + } Mail::send( [], [], @@ -254,4 +267,9 @@ function send_user_an_email(MailMessage $mail, string $email): void ->subject($mail->subject) ->html((string) $mail->render()) ); + +} +function isEmailEnabled($notifiable) +{ + return data_get($notifiable, 'smtp_enabled') || data_get($notifiable, 'resend_enabled') || data_get($notifiable, 'use_instance_email_settings'); } diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php index ab266da2c..7691ca7d6 100644 --- a/bootstrap/helpers/subscriptions.php +++ b/bootstrap/helpers/subscriptions.php @@ -56,21 +56,19 @@ function isSubscriptionActive() if (!$subscription) { return false; } - if (config('subscription.provider') === 'lemon') { + if (isLemon()) { return $subscription->lemon_status === 'active'; } - if (config('subscription.provider') === 'stripe') { + // if (isPaddle()) { + // return $subscription->paddle_status === 'active'; + // } + if (isStripe()) { return $subscription->stripe_invoice_paid === true && $subscription->stripe_cancel_at_period_end === false; } return false; - // if (config('subscription.provider') === 'paddle') { - // return $subscription->paddle_status === 'active'; - // } - } function isSubscriptionOnGracePeriod() { - $team = currentTeam(); if (!$team) { return false; @@ -79,12 +77,12 @@ function isSubscriptionOnGracePeriod() if (!$subscription) { return false; } - if (config('subscription.provider') === 'lemon') { + if (isLemon()) { $is_still_grace_period = $subscription->lemon_ends_at && Carbon::parse($subscription->lemon_ends_at) > Carbon::now(); return $is_still_grace_period; } - if (config('subscription.provider') === 'stripe') { + if (isStripe()) { return $subscription->stripe_cancel_at_period_end; } return false; @@ -93,10 +91,22 @@ function subscriptionProvider() { return config('subscription.provider'); } +function isLemon() +{ + return config('subscription.provider') === 'lemon'; +} +function isStripe() +{ + return config('subscription.provider') === 'stripe'; +} +function isPaddle() +{ + return config('subscription.provider') === 'paddle'; +} function getStripeCustomerPortalSession(Team $team) { Stripe::setApiKey(config('subscription.stripe_api_key')); - $return_url = route('team.show'); + $return_url = route('team.index'); $stripe_customer_id = $team->subscription->stripe_customer_id; $session = \Stripe\BillingPortal\Session::create([ 'customer' => $stripe_customer_id, @@ -124,6 +134,6 @@ function allowedPathsForBoardingAccounts() return [ ...allowedPathsForUnsubscribedAccounts(), 'boarding', - 'livewire/message/boarding', + 'livewire/message/boarding.index', ]; } diff --git a/composer.json b/composer.json index bcb74242b..b4164bade 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ "laravel/pint": "^v1.8.0", "mockery/mockery": "^1.5.1", "nunomaduro/collision": "^v7.4.0", - "pestphp/pest": "^v2.4.0", + "pestphp/pest": "^2.16", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^10.0.19", "serversideup/spin": "^v1.1.0", diff --git a/composer.lock b/composer.lock index 871f9f577..3ab2d9fa4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "da14dce99d76abcaaa6393166eda049a", + "content-hash": "dbb08df7a80c46ce2b9b9fa397ed71c1", "packages": [ { "name": "aws/aws-crt-php", @@ -6654,16 +6654,16 @@ }, { "name": "symfony/console", - "version": "v6.3.2", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "aa5d64ad3f63f2e48964fc81ee45cb318a723898" + "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/aa5d64ad3f63f2e48964fc81ee45cb318a723898", - "reference": "aa5d64ad3f63f2e48964fc81ee45cb318a723898", + "url": "https://api.github.com/repos/symfony/console/zipball/eca495f2ee845130855ddf1cf18460c38966c8b6", + "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6", "shasum": "" }, "require": { @@ -6724,7 +6724,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.3.2" + "source": "https://github.com/symfony/console/tree/v6.3.4" }, "funding": [ { @@ -6740,7 +6740,7 @@ "type": "tidelift" } ], - "time": "2023-07-19T20:17:28+00:00" + "time": "2023-08-16T10:10:12+00:00" }, { "name": "symfony/css-selector", @@ -7761,16 +7761,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", "shasum": "" }, "require": { @@ -7785,7 +7785,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -7823,7 +7823,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" }, "funding": [ { @@ -7839,7 +7839,7 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-iconv", @@ -7926,16 +7926,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + "reference": "875e90aeea2777b6f135677f618529449334a612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", "shasum": "" }, "require": { @@ -7947,7 +7947,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -7987,7 +7987,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" }, "funding": [ { @@ -8003,7 +8003,7 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-idn", @@ -8094,16 +8094,16 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", "shasum": "" }, "require": { @@ -8115,7 +8115,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8158,7 +8158,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" }, "funding": [ { @@ -8174,20 +8174,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "42292d99c55abe617799667f454222c54c60e229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", "shasum": "" }, "require": { @@ -8202,7 +8202,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8241,7 +8241,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" }, "funding": [ { @@ -8257,7 +8257,7 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-07-28T09:04:16+00:00" }, { "name": "symfony/polyfill-php72", @@ -8337,16 +8337,16 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", "shasum": "" }, "require": { @@ -8355,7 +8355,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -8400,7 +8400,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" }, "funding": [ { @@ -8416,7 +8416,7 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-php83", @@ -8579,16 +8579,16 @@ }, { "name": "symfony/process", - "version": "v6.3.2", + "version": "v6.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d" + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d", - "reference": "c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d", + "url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54", + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54", "shasum": "" }, "require": { @@ -8620,7 +8620,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.3.2" + "source": "https://github.com/symfony/process/tree/v6.3.4" }, "funding": [ { @@ -8636,7 +8636,7 @@ "type": "tidelift" } ], - "time": "2023-07-12T16:00:22+00:00" + "time": "2023-08-07T10:39:22+00:00" }, { "name": "symfony/psr-http-message-bridge", @@ -9982,16 +9982,16 @@ "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.2.5", + "version": "v7.2.6", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "4d7ad5b6564f63baa1b948ecad05439f22880942" + "reference": "7f372b5bb59b4271adedc67d3129df29b84c4173" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/4d7ad5b6564f63baa1b948ecad05439f22880942", - "reference": "4d7ad5b6564f63baa1b948ecad05439f22880942", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7f372b5bb59b4271adedc67d3129df29b84c4173", + "reference": "7f372b5bb59b4271adedc67d3129df29b84c4173", "shasum": "" }, "require": { @@ -10005,19 +10005,19 @@ "phpunit/php-code-coverage": "^10.1.3", "phpunit/php-file-iterator": "^4.0.2", "phpunit/php-timer": "^6.0", - "phpunit/phpunit": "^10.3.1", + "phpunit/phpunit": "^10.3.2", "sebastian/environment": "^6.0.1", - "symfony/console": "^6.3.2", - "symfony/process": "^6.3.2" + "symfony/console": "^6.3.4", + "symfony/process": "^6.3.4" }, "require-dev": { "doctrine/coding-standard": "^12.0.0", "ext-pcov": "*", "ext-posix": "*", "infection/infection": "^0.27.0", - "phpstan/phpstan": "^1.10.26", - "phpstan/phpstan-deprecation-rules": "^1.1.3", - "phpstan/phpstan-phpunit": "^1.3.13", + "phpstan/phpstan": "^1.10.32", + "phpstan/phpstan-deprecation-rules": "^1.1.4", + "phpstan/phpstan-phpunit": "^1.3.14", "phpstan/phpstan-strict-rules": "^1.5.1", "squizlabs/php_codesniffer": "^3.7.2", "symfony/filesystem": "^6.3.1" @@ -10061,7 +10061,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.2.5" + "source": "https://github.com/paratestphp/paratest/tree/v7.2.6" }, "funding": [ { @@ -10073,7 +10073,7 @@ "type": "paypal" } ], - "time": "2023-08-08T13:23:59+00:00" + "time": "2023-08-29T07:47:39+00:00" }, { "name": "fakerphp/faker", @@ -10707,24 +10707,24 @@ }, { "name": "pestphp/pest", - "version": "v2.16.0", + "version": "v2.16.1", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "cbd6a650576714c673dbb0575989663f7f5c8b6d" + "reference": "55b92666482b7d4320b7869c4eea7333d35c5631" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/cbd6a650576714c673dbb0575989663f7f5c8b6d", - "reference": "cbd6a650576714c673dbb0575989663f7f5c8b6d", + "url": "https://api.github.com/repos/pestphp/pest/zipball/55b92666482b7d4320b7869c4eea7333d35c5631", + "reference": "55b92666482b7d4320b7869c4eea7333d35c5631", "shasum": "" }, "require": { - "brianium/paratest": "^7.2.5", + "brianium/paratest": "^7.2.6", "nunomaduro/collision": "^7.8.1", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest-plugin": "^2.0.1", - "pestphp/pest-plugin-arch": "^2.3.1", + "pestphp/pest-plugin": "^2.1.1", + "pestphp/pest-plugin-arch": "^2.3.3", "php": "^8.1.0", "phpunit/phpunit": "^10.3.2" }, @@ -10734,8 +10734,8 @@ }, "require-dev": { "pestphp/pest-dev-tools": "^2.16.0", - "pestphp/pest-plugin-type-coverage": "^2.0.0", - "symfony/process": "^6.3.2" + "pestphp/pest-plugin-type-coverage": "^2.2.0", + "symfony/process": "^6.3.4" }, "bin": [ "bin/pest" @@ -10793,7 +10793,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v2.16.0" + "source": "https://github.com/pestphp/pest/tree/v2.16.1" }, "funding": [ { @@ -10805,7 +10805,7 @@ "type": "github" } ], - "time": "2023-08-21T08:42:07+00:00" + "time": "2023-08-29T09:30:36+00:00" }, { "name": "pestphp/pest-plugin", diff --git a/config/constants.php b/config/constants.php index 6cb455265..0021f8a5c 100644 --- a/config/constants.php +++ b/config/constants.php @@ -1,7 +1,7 @@ [ - 'confirmation_valid_for_minutes' => 10, + 'expiration' => 10, ], 'invitation' => [ 'link' => [ @@ -11,9 +11,18 @@ ], 'limits' => [ 'server' => [ + 'zero' => 0, + 'self-hosted' => 999999999999, 'basic' => 1, - 'pro' => 3, - 'ultimate' => 9999999999999999999, + 'pro' => 10, + 'ultimate' => 25, + ], + 'email' => [ + 'zero' => false, + 'self-hosted' => true, + 'basic' => false, + 'pro' => true, + 'ultimate' => true, ], ], ]; diff --git a/config/coolify.php b/config/coolify.php index f3485f0da..bb9e67364 100644 --- a/config/coolify.php +++ b/config/coolify.php @@ -3,7 +3,7 @@ return [ 'self_hosted' => env('SELF_HOSTED', true), 'waitlist' => env('WAITLIST', false), - 'license_url' => 'https://license.coolify.io', + 'license_url' => 'https://licenses.coollabs.io', 'mux_enabled' => env('MUX_ENABLED', true), 'dev_webhook' => env('SERVEO_URL'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), diff --git a/config/sentry.php b/config/sentry.php index 12c418325..3a4d999a5 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => trim(exec('jq -r .coolify.v4.version versions.json 2>/dev/null')) ?? 'unknown', + 'release' => '4.0.0-beta.21', 'server_name' => env('APP_ID', 'coolify'), // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/session.php b/config/session.php index e15cee1f4..447670931 100644 --- a/config/session.php +++ b/config/session.php @@ -18,7 +18,7 @@ | */ - 'driver' => env('SESSION_DRIVER', 'database'), + 'driver' => env('SESSION_DRIVER', 'redis'), /* |-------------------------------------------------------------------------- diff --git a/config/version.php b/config/version.php index 7ea9c5731..06255e53e 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ string('stripe_plan_id')->nullable()->after('stripe_cancel_at_period_end'); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('subscriptions', function (Blueprint $table) { + $table->dropColumn('stripe_plan_id'); + }); + } +}; diff --git a/database/migrations/2023_08_22_071052_add_resend_as_email.php b/database/migrations/2023_08_22_071052_add_resend_as_email.php new file mode 100644 index 000000000..fd7368bfe --- /dev/null +++ b/database/migrations/2023_08_22_071052_add_resend_as_email.php @@ -0,0 +1,30 @@ +boolean('resend_enabled')->default(false); + $table->text('resend_api_key')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('resend_enabled'); + $table->dropColumn('resend_api_key'); + }); + } +}; diff --git a/database/migrations/2023_08_22_071053_add_resend_as_email_to_teams.php b/database/migrations/2023_08_22_071053_add_resend_as_email_to_teams.php new file mode 100644 index 000000000..72be88b2c --- /dev/null +++ b/database/migrations/2023_08_22_071053_add_resend_as_email_to_teams.php @@ -0,0 +1,32 @@ +boolean('resend_enabled')->default(false); + $table->text('resend_api_key')->nullable(); + $table->boolean('use_instance_email_settings')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('teams', function (Blueprint $table) { + $table->dropColumn('resend_enabled'); + $table->dropColumn('resend_api_key'); + $table->dropColumn('use_instance_email_settings'); + }); + } +}; diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php index 09a420504..8b6ee3141 100644 --- a/database/seeders/ProductionSeeder.php +++ b/database/seeders/ProductionSeeder.php @@ -45,60 +45,62 @@ public function run(): void ]); } - // Save SSH Keys for the Coolify Host - $coolify_key_name = "id.root@host.docker.internal"; - $coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}"); + if (config('app.name') !== 'coolify-cloud') { + // Save SSH Keys for the Coolify Host + $coolify_key_name = "id.root@host.docker.internal"; + $coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}"); - if ($coolify_key) { - PrivateKey::updateOrCreate( - [ + if ($coolify_key) { + PrivateKey::updateOrCreate( + [ + 'id' => 0, + 'name' => 'localhost\'s key', + 'description' => 'The private key for the Coolify host machine (localhost).', + 'team_id' => 0, + ], + ['private_key' => $coolify_key] + ); + } else { + echo "No SSH key found for the Coolify host machine (localhost).\n"; + echo "Please generate one and save it in /data/coolify/ssh/keys/{$coolify_key_name}\n"; + echo "Then try to install again.\n"; + exit(1); + } + // Add Coolify host (localhost) as Server if it doesn't exist + if (Server::find(0) == null) { + $server_details = [ 'id' => 0, - 'name' => 'localhost\'s key', - 'description' => 'The private key for the Coolify host machine (localhost).', + 'name' => "localhost", + 'description' => "This is the server where Coolify is running on. Don't delete this!", + 'user' => 'root', + 'ip' => "host.docker.internal", 'team_id' => 0, - ], - ['private_key' => $coolify_key] - ); - } else { - echo "No SSH key found for the Coolify host machine (localhost).\n"; - echo "Please generate one and save it in /data/coolify/ssh/keys/{$coolify_key_name}\n"; - echo "Then try to install again.\n"; - exit(1); + 'private_key_id' => 0 + ]; + $server_details['proxy'] = ServerMetadata::from([ + 'type' => ProxyTypes::TRAEFIK_V2->value, + 'status' => ProxyStatus::EXITED->value + ]); + $server = Server::create($server_details); + $server->settings->is_reachable = true; + $server->settings->is_usable = true; + $server->settings->save(); + } else { + $server = Server::find(0); + $server->settings->is_reachable = true; + $server->settings->is_usable = true; + $server->settings->save(); + } + if (StandaloneDocker::find(0) == null) { + StandaloneDocker::create([ + 'id' => 0, + 'name' => 'localhost-coolify', + 'network' => 'coolify', + 'server_id' => 0, + ]); + } } - // Add Coolify host (localhost) as Server if it doesn't exist - if (Server::find(0) == null) { - $server_details = [ - 'id' => 0, - 'name' => "localhost", - 'description' => "This is the server where Coolify is running on. Don't delete this!", - 'user' => 'root', - 'ip' => "host.docker.internal", - 'team_id' => 0, - 'private_key_id' => 0 - ]; - $server_details['proxy'] = ServerMetadata::from([ - 'type' => ProxyTypes::TRAEFIK_V2->value, - 'status' => ProxyStatus::EXITED->value - ]); - $server = Server::create($server_details); - $server->settings->is_reachable = true; - $server->settings->is_usable = true; - $server->settings->save(); - } else { - $server = Server::find(0); - $server->settings->is_reachable = true; - $server->settings->is_usable = true; - $server->settings->save(); - } - if (StandaloneDocker::find(0) == null) { - StandaloneDocker::create([ - 'id' => 0, - 'name' => 'localhost-coolify', - 'network' => 'coolify', - 'server_id' => 0, - ]); - } try { $settings = InstanceSettings::get(); if (is_null($settings->public_ipv4)) { diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index 19d3aa42e..2ac615cc0 100644 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -15,10 +15,12 @@ public function run(): void 'email' => 'test@example.com', ]); User::factory()->create([ + 'id' => 1, 'name' => 'Normal User (but in root team)', 'email' => 'test2@example.com', ]); User::factory()->create([ + 'id' => 2, 'name' => 'Normal User (not in root team)', 'email' => 'test3@example.com', ]); diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 710f44f2c..6268f9963 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,7 +1,7 @@ version: '3.8' services: coolify: - image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-4.0.0-nightly.0}" + image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-4.0.0-beta.20}" volumes: - type: bind source: /data/coolify/source/.env diff --git a/phpunit.xml b/phpunit.xml index eb13aff19..45cb69439 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,31 +1,28 @@ - - - - ./tests/Unit - - - ./tests/Feature - - - - - ./app - - - - - - - - - - - - - + + + + ./tests/Unit + + + ./tests/Feature + + + + + + + + + + + + + + + + + ./app + + diff --git a/resources/js/components/MagicBar.vue b/resources/js/components/MagicBar.vue index a0e8f762c..5719bc1d6 100644 --- a/resources/js/components/MagicBar.vue +++ b/resources/js/components/MagicBar.vue @@ -408,6 +408,12 @@ const magicActions = [{ name: 'Goto: Switch Teams', icon: 'goto', sequence: ['main', 'redirect'] +}, +{ + id: 23, + name: 'Goto: Boarding process', + icon: 'goto', + sequence: ['main', 'redirect'] } ] const initialState = { @@ -635,6 +641,9 @@ async function redirect() { case 22: targetUrl.pathname = `/team` break; + case 23: + targetUrl.pathname = `/boarding` + break; } window.location.href = targetUrl; } diff --git a/resources/views/auth/confirm-password.blade.php b/resources/views/auth/confirm-password.blade.php index 2cbc6dd72..dd1331d43 100644 --- a/resources/views/auth/confirm-password.blade.php +++ b/resources/views/auth/confirm-password.blade.php @@ -8,7 +8,7 @@
@csrf - {{ __('auth.confirm_password') }} diff --git a/resources/views/auth/waitlist.blade.php b/resources/views/auth/waitlist.blade.php deleted file mode 100644 index 497bb0868..000000000 --- a/resources/views/auth/waitlist.blade.php +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/views/boarding.blade.php b/resources/views/boarding.blade.php deleted file mode 100644 index bdf229935..000000000 --- a/resources/views/boarding.blade.php +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - Close - - - - diff --git a/resources/views/components/forms/button.blade.php b/resources/views/components/forms/button.blade.php index a2350beda..f619e3ba1 100644 --- a/resources/views/components/forms/button.blade.php +++ b/resources/views/components/forms/button.blade.php @@ -12,7 +12,9 @@ @if ($attributes->get('type') === 'submit') @else - + @if ($attributes->has('wire:click')) + + @endif @endif diff --git a/resources/views/components/layout-simple.blade.php b/resources/views/components/layout-simple.blade.php index cf1328b32..538b357f0 100644 --- a/resources/views/components/layout-simple.blade.php +++ b/resources/views/components/layout-simple.blade.php @@ -1,68 +1 @@ - - - - - - - - - @env('local') - Coolify - localhost - - @else - {{ $title ?? 'Coolify' }} - - @endenv - - @vite(['resources/js/app.js', 'resources/css/app.css']) - - @livewireStyles - - - - @livewireScripts - -
- {{ $slot }} -
- - - - - - +@extends('layouts.simple') diff --git a/resources/views/components/layout-subscription.blade.php b/resources/views/components/layout-subscription.blade.php index a3b0bf806..5fdd838af 100644 --- a/resources/views/components/layout-subscription.blade.php +++ b/resources/views/components/layout-subscription.blade.php @@ -1,86 +1 @@ - - - - - - - - - @env('local') - Coolify - localhost - -@else - {{ $title ?? 'Coolify' }} - - @endenv - - @vite(['resources/js/app.js', 'resources/css/app.css']) - - @livewireStyles - - - - @livewireScripts - - @if (isSubscriptionOnGracePeriod()) -
- -
- - @else - - @endif - -
- {{ $slot }} -
- - - - - +@extends('layouts.subscription') diff --git a/resources/views/components/layout.blade.php b/resources/views/components/layout.blade.php index 32fc1c73f..f16b07f50 100644 --- a/resources/views/components/layout.blade.php +++ b/resources/views/components/layout.blade.php @@ -1,133 +1 @@ - - - - - - - - - @env('local') - Coolify - localhost - -@else - {{ $title ?? 'Coolify' }} - - @endenv - - @vite(['resources/js/app.js', 'resources/css/app.css']) - - @livewireStyles - - - - @livewireScripts - @auth - - -
- -
-
- {{ $slot }} -
- - - @endauth - @guest - {{ $slot }} - @endguest - - - +@extends('layouts.app') diff --git a/resources/views/components/limit-reached.blade.php b/resources/views/components/limit-reached.blade.php index c2c4b4b0e..6b0f6f2ad 100644 --- a/resources/views/components/limit-reached.blade.php +++ b/resources/views/components/limit-reached.blade.php @@ -1,6 +1,6 @@
You have reached the limit of {{ $name }} you can create. - Please upgrade your + Please upgrade your subscription to create more {{ $name }}.
diff --git a/resources/views/components/navbar-subscription.blade.php b/resources/views/components/navbar-subscription.blade.php index 08248e61e..73e67410b 100644 --- a/resources/views/components/navbar-subscription.blade.php +++ b/resources/views/components/navbar-subscription.blade.php @@ -1,6 +1,15 @@ @auth
-
- -
-
-
- -
-
- - - -
-
- - - -
-
- - -
+
- @if (data_get($model, 'smtp_enabled')) -

Subscribe to events

+ @if ($this->sharedEmailEnabled) +
+ +
+ @endif + @if (!$team->use_instance_email_settings) +
+ + + + Save + + +
+
+ +
SMTP Server
+
+ +
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+ + Save + +
+
+
+
+
+ +
Resend
+
+ +
+
+
+
+
+
+ +
+
+
+ + Save + +
+
+
+
+
+ @endif + @if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings')) +

Subscribe to events

@if (isDev()) - + @endif

General

-

Applications

- +

Databases

-
@endif diff --git a/resources/views/livewire/profile/form.blade.php b/resources/views/livewire/profile/form.blade.php index 343624f00..4a7da9df2 100644 --- a/resources/views/livewire/profile/form.blade.php +++ b/resources/views/livewire/profile/form.blade.php @@ -1,7 +1,7 @@
-

General

+

General

Save
diff --git a/resources/views/livewire/project/application/deployment-logs.blade.php b/resources/views/livewire/project/application/deployment-logs.blade.php index 1a0c5116f..f354083b3 100644 --- a/resources/views/livewire/project/application/deployment-logs.blade.php +++ b/resources/views/livewire/project/application/deployment-logs.blade.php @@ -18,12 +18,12 @@ class="scrollbar flex flex-col-reverse w-full overflow-y-auto border border-dott @if (decode_remote_command_output($application_deployment_queue)->count() > 0) @foreach (decode_remote_command_output($application_deployment_queue) as $line)
$line['type'] == 'stdout', 'text-error' => $line['type'] == 'stderr', 'text-warning' => $line['hidden'], ])>[{{ $line['timestamp'] }}] @if ($line['hidden']) -
Command: {{ $line['command'] }}
Output: +
COMMAND:
{{ $line['command'] }}

OUTPUT: @endif{{ $line['output'] }}@if ($line['hidden']) @endif
diff --git a/resources/views/livewire/project/new/github-private-repository.blade.php b/resources/views/livewire/project/new/github-private-repository.blade.php index 72950718b..09030b76f 100644 --- a/resources/views/livewire/project/new/github-private-repository.blade.php +++ b/resources/views/livewire/project/new/github-private-repository.blade.php @@ -1,13 +1,12 @@

Create a new Application

- - + Add New GitHub App - - + + + Add New GitHub App +
Deploy any public or private git repositories through a GitHub App.
- @if ($github_apps->count() == 0) + @if ($github_apps->count() !== 0)
@if ($current_step === 'github_apps')
    @@ -25,7 +24,7 @@ {{ $ghapp->name }}
{{ $ghapp->http_url }}
-
@@ -39,7 +38,7 @@ class="loading loading-xs text-warning loading-spinner">
{{ data_get($ghapp, 'html_url') }}
- Loading... diff --git a/resources/views/livewire/project/new/select.blade.php b/resources/views/livewire/project/new/select.blade.php index bdb6ac4ad..be76d5e66 100644 --- a/resources/views/livewire/project/new/select.blade.php +++ b/resources/views/livewire/project/new/select.blade.php @@ -70,7 +70,6 @@ - @endif @if ($current_step === 'servers') @@ -79,7 +78,7 @@
  • Select a Server
  • Select a Destination
  • -
    +
    @forelse($servers as $server)
    @@ -108,19 +107,29 @@
  • Select a Server
  • Select a Destination
  • -
    - @foreach ($destinations as $destination) +
    + @foreach ($standaloneDockers as $standaloneDocker)
    + wire:click="set_destination('{{ $standaloneDocker->uuid }}')">
    -
    - {{ $destination->name }} +
    + Standalone Docker ({{ $standaloneDocker->name }})
    - {{ $destination->network }}
    + network: {{ $standaloneDocker->network }}
    @endforeach + @foreach ($swarmDockers as $swarmDocker) +
    +
    +
    + Swarm Docker ({{ $swarmDocker->name }}) +
    +
    +
    + @endforeach
    @endif
    diff --git a/resources/views/livewire/server/all.blade.php b/resources/views/livewire/server/all.blade.php new file mode 100644 index 000000000..8ad9c82a3 --- /dev/null +++ b/resources/views/livewire/server/all.blade.php @@ -0,0 +1,54 @@ +
    +
    +

    Servers

    + + + Add + +
    +
    All Servers
    + +
    diff --git a/resources/views/livewire/server/proxy/deploy.blade.php b/resources/views/livewire/server/proxy/deploy.blade.php index 685cddbdf..9e46ce0cc 100644 --- a/resources/views/livewire/server/proxy/deploy.blade.php +++ b/resources/views/livewire/server/proxy/deploy.blade.php @@ -20,7 +20,7 @@ @if (data_get($server, 'proxy.status') === 'running')
    -
    -
    -

    Transactional Emails

    +
    +

    Transactional/Shared Email

    +
    +
    Email settings for password resets, invitations, shared with Pro+ subscribers etc.
    + +
    + + Save - @if ($settings->smtp_enabled) - - Send Test Email - - @endif -
    -
    SMTP settings for password resets, invitations, etc.
    -
    - -
    -
    -
    - - - -
    -
    - - - -
    -
    - - -
    + @if ($settings->resend_enabled || $settings->smtp_enabled) + + Send Test Email + + @endif
    +
    +
    + +
    SMTP Server
    +
    + +
    +
    +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    +
    + + Save + +
    +
    +
    +
    +
    + +
    Resend
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + Save + +
    +
    +
    +
    +
    diff --git a/resources/views/livewire/source/github/change.blade.php b/resources/views/livewire/source/github/change.blade.php index 1fc17d985..5b971c5f4 100644 --- a/resources/views/livewire/source/github/change.blade.php +++ b/resources/views/livewire/source/github/change.blade.php @@ -4,24 +4,22 @@

    This source will be deleted. It is not reversible.
    Please think again.

    -
    + -
    Your Private GitHub App for private repositories.
    - @if ($github_app->app_id) +
    Your Private GitHub App for private repositories.
    + @if (data_get($github_app, 'app_id')) + @if (!data_get($github_app, 'installation_id')) +
    + + + + You must complete this step before you can use this source! +
    + + Install Repositories on GitHub + + @else
    + @endif + @else +
    + + + + You must complete this step before you can use this source! +
    -
    -

    Register a GitHub App

    - - Register a - GitHub - Application - -
    +

    Register a GitHub App

    You need to register a GitHub App before using this source.
    -
    - - @if ($ipv4) - - @endif - @if ($ipv6) - - @endif - @if ($fqdn) - - @endif - -
    + @if (!isCloud() || isDev()) +
    + + @if ($ipv4) + + @endif + @if ($ipv6) + + @endif + @if ($fqdn) + + @endif + @if (config('app.url')) + + @endif + + + Register + +
    + @else + + Register Now + + @endif
    @@ -100,27 +127,6 @@
    -
    - - -
    -
    - - -
    -
    - @if ($github_app->html_url === 'https://github.com') - - - @else - - - @endif -
    - -
    + diff --git a/resources/views/server/private-key.blade.php b/resources/views/server/private-key.blade.php index 3c42f98f0..d1b22b16c 100644 --- a/resources/views/server/private-key.blade.php +++ b/resources/views/server/private-key.blade.php @@ -1,4 +1,4 @@ - + diff --git a/resources/views/settings/configuration.blade.php b/resources/views/settings/configuration.blade.php index 86e24480f..06e5ef9f8 100644 --- a/resources/views/settings/configuration.blade.php +++ b/resources/views/settings/configuration.blade.php @@ -5,9 +5,9 @@ General Backup + @click.prevent="activeTab = 'backup'; window.location.hash = 'backup'" href="#">Instance Backup SMTP + @click.prevent="activeTab = 'smtp'; window.location.hash = 'smtp'" href="#">Transactional/Shared Email
    diff --git a/resources/views/source/all.blade.php b/resources/views/source/all.blade.php index aea37db7c..0ffb8ca11 100644 --- a/resources/views/source/all.blade.php +++ b/resources/views/source/all.blade.php @@ -7,23 +7,22 @@ -
    +
    {{ $source->name }}
    @if (is_null($source->app_id)) - Not registered + Configuration is not finished @endif
    -
    @endif @if ($source->getMorphClass() === 'App\Models\GitlabApp') -
    +
    {{ $source->name }}
    @if (is_null($source->app_id)) - Not registered + Configuration is not finished @endif
    diff --git a/resources/views/subscription/cancel.blade.php b/resources/views/subscription/cancel.blade.php deleted file mode 100644 index dc7e3b55b..000000000 --- a/resources/views/subscription/cancel.blade.php +++ /dev/null @@ -1,3 +0,0 @@ - - Cancel - diff --git a/resources/views/subscription/show.blade.php b/resources/views/subscription/index.blade.php similarity index 52% rename from resources/views/subscription/show.blade.php rename to resources/views/subscription/index.blade.php index 669e94c0e..e13d856de 100644 --- a/resources/views/subscription/show.blade.php +++ b/resources/views/subscription/index.blade.php @@ -3,16 +3,23 @@
    -

    Subscription

    +

    Subscription

    Currently active team: {{ session('currentTeam.name') }}
    - @if(request()->query->get('cancelled')) -
    Something went wrong. Please try again.
    - @endif + @if (request()->query->get('cancelled')) +
    + + + + Something went wrong with your subscription. Please try again or contact support. +
    + @endif @if (config('subscription.provider') !== null) @endif diff --git a/resources/views/subscription/success.blade.php b/resources/views/subscription/success.blade.php deleted file mode 100644 index 9cb5911cb..000000000 --- a/resources/views/subscription/success.blade.php +++ /dev/null @@ -1,3 +0,0 @@ - - Success - diff --git a/resources/views/team/show.blade.php b/resources/views/team/index.blade.php similarity index 83% rename from resources/views/team/show.blade.php rename to resources/views/team/index.blade.php index 25a465452..3bb81512b 100644 --- a/resources/views/team/show.blade.php +++ b/resources/views/team/index.blade.php @@ -3,7 +3,7 @@ ->user() ->currentTeam()" /> - @if (is_cloud()) + @if (isCloud())

    Subscription

    @if (data_get(currentTeam(), @@ -11,7 +11,7 @@ @else Subscribe Now + href="{{ route('subscription.index') }}">Subscribe Now @endif diff --git a/resources/views/team/notifications.blade.php b/resources/views/team/notifications.blade.php index 875102d16..4714c30a7 100644 --- a/resources/views/team/notifications.blade.php +++ b/resources/views/team/notifications.blade.php @@ -12,7 +12,7 @@
    - user() ->currentTeam()" />
    diff --git a/routes/web.php b/routes/web.php index ce98b65b7..d59b9a48d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,6 +6,12 @@ use App\Http\Controllers\MagicController; use App\Http\Controllers\ProjectController; use App\Http\Controllers\ServerController; +use App\Http\Livewire\Boarding\Index; +use App\Http\Livewire\Boarding\Server as BoardingServer; +use App\Http\Livewire\Dashboard; +use App\Http\Livewire\Server\All; +use App\Http\Livewire\Server\Show; +use App\Http\Livewire\Waitlist\Index as WaitlistIndex; use App\Models\GithubApp; use App\Models\GitlabApp; use App\Models\InstanceSettings; @@ -23,7 +29,10 @@ Route::post('/forgot-password', function (Request $request) { if (is_transactional_emails_active()) { - set_transanctional_email_settings(); + $type = set_transanctional_email_settings(); + if (!$type) { + return response()->json(['message' => 'Transactional emails are not active'], 400); + } $request->validate([Fortify::email() => 'required|email']); $status = Password::broker(config('fortify.passwords'))->sendResetLink( $request->only(Fortify::email()) @@ -38,7 +47,7 @@ } return response()->json(['message' => 'Transactional emails are not active'], 400); })->name('password.forgot'); -Route::get('/waitlist', [Controller::class, 'waitlist'])->name('auth.waitlist'); +Route::get('/waitlist', WaitlistIndex::class)->name('waitlist.index'); Route::prefix('magic')->middleware(['auth'])->group(function () { Route::get('/servers', [MagicController::class, 'servers']); @@ -71,13 +80,9 @@ }); Route::middleware(['auth'])->group(function () { - Route::get('/servers', fn () => view('server.all', [ - 'servers' => Server::ownedByCurrentTeam()->get() - ]))->name('server.all'); + Route::get('/servers', All::class)->name('server.all'); Route::get('/server/new', [ServerController::class, 'new_server'])->name('server.create'); - Route::get('/server/{server_uuid}', fn () => view('server.show', [ - 'server' => Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail(), - ]))->name('server.show'); + Route::get('/server/{server_uuid}', Show::class)->name('server.show'); Route::get('/server/{server_uuid}/proxy', fn () => view('server.proxy', [ 'server' => Server::ownedByCurrentTeam(['name', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail(), ]))->name('server.proxy'); @@ -92,18 +97,16 @@ Route::middleware(['auth'])->group(function () { - Route::get('/', [Controller::class, 'dashboard'])->name('dashboard'); - Route::get('/boarding', [Controller::class, 'boarding'])->name('boarding'); - Route::middleware(['throttle:force-password-reset'])->group(function() { + Route::get('/', Dashboard::class)->name('dashboard'); + Route::get('/boarding', Index::class)->name('boarding'); + Route::middleware(['throttle:force-password-reset'])->group(function () { Route::get('/force-password-reset', [Controller::class, 'force_passoword_reset'])->name('auth.force-password-reset'); }); - Route::get('/subscription', [Controller::class, 'subscription'])->name('subscription.show'); - Route::get('/subscription/success', fn () => view('subscription.success'))->name('subscription.success'); - Route::get('/subscription/cancel', fn () => view('profile'))->name('subscription.cancel'); + Route::get('/subscription', [Controller::class, 'subscription'])->name('subscription.index'); Route::get('/settings', [Controller::class, 'settings'])->name('settings.configuration'); Route::get('/settings/license', [Controller::class, 'license'])->name('settings.license'); Route::get('/profile', fn () => view('profile', ['request' => request()]))->name('profile'); - Route::get('/team', [Controller::class, 'team'])->name('team.show'); + Route::get('/team', [Controller::class, 'team'])->name('team.index'); Route::get('/team/new', fn () => view('team.create'))->name('team.create'); Route::get('/team/notifications', fn () => view('team.notifications'))->name('team.notifications'); Route::get('/team/storages', [Controller::class, 'storages'])->name('team.storages.all'); @@ -144,6 +147,26 @@ if ($settings->public_ipv6) { $ipv6 = 'http://' . $settings->public_ipv6 . ':' . config('app.port'); } + if ($github_app->installation_id && session('from')) { + $source_id = data_get(session('from'), 'source_id'); + if (!$source_id || $github_app->id !== $source_id) { + session()->forget('from'); + } else { + $parameters = data_get(session('from'), 'parameters'); + $back = data_get(session('from'), 'back'); + $environment_name = data_get($parameters, 'environment_name'); + $project_uuid = data_get($parameters, 'project_uuid'); + $type = data_get($parameters, 'type'); + $destination = data_get($parameters, 'destination'); + session()->forget('from'); + return redirect()->route($back, [ + 'environment_name' => $environment_name, + 'project_uuid' => $project_uuid, + 'type' => $type, + 'destination' => $destination, + ]); + } + } return view('source.github.show', [ 'github_app' => $github_app, 'name' => $name, diff --git a/routes/webhooks.php b/routes/webhooks.php index 026b2ebc3..5d9257094 100644 --- a/routes/webhooks.php +++ b/routes/webhooks.php @@ -181,7 +181,7 @@ 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'))) { + if ($found && !$found->verified && $found->created_at > now()->subMinutes(config('constants.waitlist.expiration'))) { $found->verified = true; $found->save(); send_internal_notification('Waitlist confirmed: ' . $email); @@ -267,9 +267,11 @@ break; case 'customer.subscription.updated': $subscriptionId = data_get($data, 'items.data.0.subscription'); + $planId = data_get($data, 'items.data.0.plan.id'); $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end'); $subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail(); $subscription->update([ + 'stripe_plan_id' => $planId, 'stripe_cancel_at_period_end' => $cancelAtPeriodEnd, ]); break; @@ -277,6 +279,8 @@ $subscriptionId = data_get($data, 'items.data.0.subscription'); $subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail(); $subscription->update([ + 'stripe_subscription_id' => null, + 'stripe_plan_id'=> null, 'stripe_cancel_at_period_end' => false, 'stripe_invoice_paid' => false, ]); diff --git a/tailwind.config.js b/tailwind.config.js index 274f50773..46bd15c02 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -41,7 +41,7 @@ module.exports = { coollabs: { primary: "#323232", "primary-focus": "#242424", - secondary: "#4338ca", + secondary: "#6B16ED", accent: "#4338ca", neutral: "#1B1D1D", "base-100": "#181818", diff --git a/tests/Feature/DockerCommandsTest.php b/tests/Feature/DockerCommandsTest.phpold similarity index 100% rename from tests/Feature/DockerCommandsTest.php rename to tests/Feature/DockerCommandsTest.phpold diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php new file mode 100644 index 000000000..e07cdfb06 --- /dev/null +++ b/tests/Feature/ExampleTest.php @@ -0,0 +1,7 @@ +get('/api/health'); + + $response->assertStatus(200); +}); diff --git a/tests/Feature/RemoteProcessTest.php b/tests/Feature/RemoteProcessTest.phpold similarity index 100% rename from tests/Feature/RemoteProcessTest.php rename to tests/Feature/RemoteProcessTest.phpold diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php new file mode 100644 index 000000000..44a4f337a --- /dev/null +++ b/tests/Unit/ExampleTest.php @@ -0,0 +1,5 @@ +toBeTrue(); +}); diff --git a/tests/Unit/RulesTest.php b/tests/Unit/RulesTest.php index 02e14ba48..585e7126c 100644 --- a/tests/Unit/RulesTest.php +++ b/tests/Unit/RulesTest.php @@ -1,5 +1,5 @@ expect(['dd', 'dump', 'ray']) + ->expect(['dd', 'dump']) ->not->toBeUsed(); diff --git a/versions.json b/versions.json index 5215932b7..d50394f5b 100644 --- a/versions.json +++ b/versions.json @@ -4,7 +4,7 @@ "version": "3.12.36" }, "v4": { - "version": "4.0.0-beta.20" + "version": "4.0.0-beta.21" } } }