Compare commits

...

53 Commits

Author SHA1 Message Date
Andras Bacsai
cc8c6c5d16 Merge pull request #1221 from coollabsio/next
UI/UX fixes
2023-09-11 13:59:19 +02:00
Andras Bacsai
4782446f42 ui: show registered users on waitlist page 2023-09-11 13:53:08 +02:00
Andras Bacsai
230155312f ui: services are not availble yet 2023-09-11 13:52:54 +02:00
Andras Bacsai
3ab4365fca ui: user should know that the public key 2023-09-11 13:42:32 +02:00
Andras Bacsai
7349068b95 Merge pull request #1220 from coollabsio/next
Email issues fix
2023-09-11 12:55:52 +02:00
Andras Bacsai
da6cc151d1 fix: email sending error 2023-09-11 12:31:31 +02:00
Andras Bacsai
50527cf0a3 fix: testing email
fix: expired invitiation link
2023-09-11 12:16:26 +02:00
Andras Bacsai
26f490bb00 Merge pull request #1219 from coollabsio/next
Fixes
2023-09-11 10:50:18 +02:00
Andras Bacsai
dc4f412227 fix: recovery code 2023-09-11 10:41:39 +02:00
Andras Bacsai
ec4234e243 fix: only send internal notifcations to enabled channels 2023-09-11 10:19:46 +02:00
Andras Bacsai
f47fcb01ce Merge pull request #1218 from coollabsio/next
feat: generate public key from private keys
2023-09-11 10:16:48 +02:00
Andras Bacsai
02f6673345 feat: generate public key from private keys 2023-09-11 10:15:45 +02:00
Andras Bacsai
e6cd8702b5 Merge pull request #1216 from coollabsio/next
v4.0.0-beta.32
2023-09-10 15:34:08 +02:00
Andras Bacsai
fda4ea8cca fix: errors in views 2023-09-10 15:33:00 +02:00
Andras Bacsai
655d004ce7 Merge pull request #1214 from coollabsio/next
v4.0.0-beta.31
2023-09-09 15:33:16 +02:00
Andras Bacsai
56981d134c fix: improve startProxy action
fix: improve installDocker action and process
feat: add noSubmit prop to custom modal
2023-09-09 15:30:46 +02:00
Andras Bacsai
b9d49d2951 fix: remove -q from docker compose 2023-09-09 13:52:40 +02:00
Andras Bacsai
0c1e7c499e Merge pull request #1213 from coollabsio/next
v4.0.0-beta.30
2023-09-09 13:23:59 +02:00
Andras Bacsai
32fead5753 version++ 2023-09-09 13:23:03 +02:00
Andras Bacsai
e5e9faba35 fix: delete database related things when delete database 2023-09-09 13:18:49 +02:00
Andras Bacsai
2852630d6c Update DatabaseBackupJob.php 2023-09-09 10:34:10 +02:00
Andras Bacsai
a4cc406114 Merge pull request #1212 from coollabsio/next
v4.0.0-beta.29
2023-09-08 18:52:41 +02:00
Andras Bacsai
53b15a5762 update sentry dsn 2023-09-08 18:40:32 +02:00
Andras Bacsai
929a4e6474 fix: coolify already exists should not throw error 2023-09-08 18:40:25 +02:00
Andras Bacsai
45b597bbab feat: cache team settings 2023-09-08 18:33:26 +02:00
Andras Bacsai
0d1a2aa5d1 update testemail command 2023-09-08 17:51:19 +02:00
Andras Bacsai
b82353d5e2 update testemail command 2023-09-08 17:42:08 +02:00
Andras Bacsai
b17c09f7a7 update testemail command 2023-09-08 17:31:02 +02:00
Andras Bacsai
f6c3fe7888 fix: test email on for admins or custom smtp 2023-09-08 17:26:59 +02:00
Andras Bacsai
2e855e030f fix: ui 2023-09-08 16:59:49 +02:00
Andras Bacsai
49f86621f4 fix: instance email settings 2023-09-08 16:56:14 +02:00
Andras Bacsai
03d9f93397 fix: retry notifications 2023-09-08 16:53:19 +02:00
Andras Bacsai
c472042a94 fix: ui 2023-09-08 16:46:53 +02:00
Andras Bacsai
9f4356f67d version++ 2023-09-08 16:28:56 +02:00
Andras Bacsai
a50317cc76 Merge pull request #1209 from coollabsio/next
v4.0.0-beta.28
2023-09-08 16:28:04 +02:00
Andras Bacsai
8afa98a1ca fix: sentry 4451028626 2023-09-08 16:19:45 +02:00
Andras Bacsai
f6737f21dd feat: developer view for env variables 2023-09-08 16:16:59 +02:00
Andras Bacsai
e4a51cc116 fix: sentry 4459819517 2023-09-08 16:16:28 +02:00
Andras Bacsai
acd78ae196 feat: Telegram topics separation 2023-09-08 14:15:28 +02:00
Andras Bacsai
953bcfb5bb fix: db backup job 2023-09-08 12:23:46 +02:00
Andras Bacsai
dacfab8b29 Update BUG_REPORT.yml 2023-09-08 12:08:16 +02:00
Andras Bacsai
48b3e99939 Merge pull request #1203 from coollabsio/next
v4.0.0-beta.27
2023-09-08 12:06:34 +02:00
Andras Bacsai
41ad67c7c9 updates 2023-09-08 12:05:15 +02:00
Andras Bacsai
b49725cb1c fix: bug 2023-09-08 09:37:58 +02:00
Andras Bacsai
75e674a966 version++ 2023-09-08 09:27:44 +02:00
Andras Bacsai
9d826d9fb4 Merge pull request #1202 from coollabsio/next
v4.0.0-beta.26
2023-09-08 09:08:19 +02:00
Andras Bacsai
0af221f9fc udpate 2023-09-08 09:06:00 +02:00
Andras Bacsai
9066c9bf90 update readme 2023-09-08 09:00:44 +02:00
Andras Bacsai
77e3208f00 feat: public database 2023-09-07 13:23:34 +02:00
Andras Bacsai
db5ecf07bd version++ 2023-09-07 09:14:27 +02:00
Andras Bacsai
7f28aa6985 Merge pull request #1201 from coollabsio/next
v4.0.0-beta.25
2023-09-07 09:04:33 +02:00
Andras Bacsai
9d53e04ce9 fix: saveModel email settings 2023-09-07 08:48:16 +02:00
Andras Bacsai
3db9a1dd6e version++ 2023-09-06 15:26:17 +02:00
76 changed files with 786 additions and 301 deletions

26
.github/ISSUE_TEMPLATE/BUG_REPORT.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Bug report
description: Create a new bug report
title: '[Bug]: '
body:
- type: textarea
attributes:
label: Description
description: A clear and concise description of the problem
validations:
required: true
- type: textarea
attributes:
label: Minimal Reproduction (if possible, example repository)
description: Please provide a step by step guide to reproduce the issue
validations:
required: true
- type: textarea
attributes:
label: Exception or Error
description: Please provide error logs if possible.
- type: input
attributes:
label: Version
description: Coolify's version (see bottom left corner).
validations:
required: true

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 🤔 Community Support (Chat)
url: https://coollabs.io/discord
about: Reach out to us on Discord.
- name: 🙋‍♂️ Feature Requests
url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests-ideas
about: All feature requests will be discussed here.

1
.gitignore vendored
View File

@@ -31,3 +31,4 @@ _ide_helper.php
_ide_helper_models.php
.rnd
/.ssh
scripts/load-test/*

View File

@@ -1,20 +1,30 @@
# Coolify v4 Beta
# About the Project
An open-source & self-hostable Heroku / Netlify alternative.
Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything.
Image if you could have the ease of a cloud but with your own servers. That is **Coolify**.
No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️
For more information, take a look at our landing page [here](https://coolify.io).
> If you are looking for previous (v3) version, it is [here](https://github.com/coollabsio/coolify/tree/v3).
# Cloud
If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io
You can easily attach your own servers, get all the automations, free email notifications, etc.
For more information & pricing, take a look at our landing page [here](https://coolify.io).
# Beta
You are checking the next-gen of Coolify, aka v4. Hi 👋
The latest version (v4) is still in beta. That does not mean it is unstable. All the features that are available are stable enough be usable in real-life.
It is still in beta, lots of improvements will come every day. Things could break, but we are working hard to make it stable as soon as possible. If you find any bugs, please report them.
Automatic updates are available, so you will receive the latest version as soon as it is released.
If you are looking for v3, check out the [v3 branch](https://github.com/coollabsio/coolify/tree/v3).
## What's new?
Well, the whole tech stack changed, core is different, so yeah, a lot (documentation incoming).
There are hundreds of people using it for managing their client's applications, freelancers, hobbyists, businesses.
# Installation
@@ -26,13 +36,19 @@ You can find the installation script [here](./scripts/install.sh).
## Support
- Twitter: [@heyandras](https://twitter.com/heyandras)
- Mastodon: [@andrasbacsai@fosstodon.org](https://fosstodon.org/@andrasbacsai)
- Email: [andras@coollabs.io](mailto:andras@coollabs.io)
- Discord: [Invitation](https://coollabs.io/discord)
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
Contact us [here](https://docs.coollabs.io/contact).
---
## Recognitions
<a href="https://news.ycombinator.com/item?id=26624341">
<img
style="width: 250px; height: 54px;" width="250" height="54"
alt="Featured on Hacker News"
src="https://hackernews-badge.vercel.app/api?id=26624341"
/>
</a>
<a href="https://www.producthunt.com/posts/coolify?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-coolify" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=338273&theme=light" alt="Coolify - An&#0032;open&#0045;source&#0032;&#0038;&#0032;self&#0045;hostable&#0032;Heroku&#0044;&#0032;Netlify&#0032;alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## 💰 Financial Contributors

View File

@@ -30,19 +30,19 @@ class StartProxy
$server->save();
$activity = remote_process([
"echo 'Creating required Docker networks...'",
"echo '####### Creating required Docker networks...'",
...$create_networks_command,
"cd $proxy_path",
"echo 'Creating Docker Compose file...'",
"echo 'Pulling docker image...'",
'docker compose pull -q',
"echo 'Stopping existing proxy...'",
"echo '####### Creating Docker Compose file...'",
"echo '####### Pulling docker image...'",
'docker compose pull',
"echo '####### Stopping existing proxy...'",
'docker compose down -v --remove-orphans',
"lsof -nt -i:80 | xargs -r kill -9",
"lsof -nt -i:443 | xargs -r kill -9",
"echo 'Starting proxy...'",
"echo '####### Starting proxy...'",
'docker compose up -d --remove-orphans',
"echo 'Proxy installed successfully...'"
"echo '####### Proxy installed successfully...'"
], $server);
return $activity;

View File

@@ -18,42 +18,42 @@ class InstallDocker
"max-file": "3"
}
}');
$found = StandaloneDocker::where('server_id', $server->id);
if ($found->count() == 0) {
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
]);
}
if (isDev()) {
$activity = remote_process([
"echo ####### Installing Prerequisites...",
"echo ####### Installing/updating Docker Engine...",
"echo ####### Configuring Docker Engine (merging existing configuration with the required)...",
"echo ####### Restarting Docker Engine...",
return remote_process([
"echo '####### Installing Prerequisites...'",
"sleep 1",
"echo '####### Installing/updating Docker Engine...'",
"echo '####### Configuring Docker Engine (merging existing configuration with the required)...'",
"sleep 4",
"echo '####### Restarting Docker Engine...'",
"ls -l /tmp"
], $server);
} else {
$activity = remote_process([
"echo ####### Installing Prerequisites...",
return remote_process([
"echo '####### Installing Prerequisites...'",
"command -v jq >/dev/null || apt-get update",
"command -v jq >/dev/null || apt install -y jq",
"echo ####### Installing/updating Docker Engine...",
"echo '####### Installing/updating Docker Engine...'",
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh",
"echo ####### Configuring Docker Engine (merging existing configuration with the required)...",
"echo '####### Configuring Docker Engine (merging existing configuration with the required)...'",
"test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json \"/etc/docker/daemon.json.original-`date +\"%Y%m%d-%H%M%S\"`\" || echo '{$config}' | base64 -d > /etc/docker/daemon.json",
"echo '{$config}' | base64 -d > /etc/docker/daemon.json.coolify",
"cat <<< $(jq . /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json.coolify",
"cat <<< $(jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json",
"echo ####### Restarting Docker Engine...",
"echo '####### Restarting Docker Engine...'",
"systemctl restart docker",
"echo ####### Creating default network...",
"docker network create --attachable coolify",
"echo ####### Done!"
"echo '####### Creating default Docker network (coolify)...'",
"docker network create --attachable coolify >/dev/null 2>&1 || true",
"echo '####### Done!'"
], $server);
$found = StandaloneDocker::where('server_id', $server->id);
if ($found->count() == 0) {
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
]);
}
}
return $activity;
}
}

View File

@@ -2,12 +2,14 @@
namespace App\Console\Commands;
use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\StandalonePostgresql;
use App\Models\TeamInvitation;
use App\Models\User;
use App\Models\Waitlist;
use App\Notifications\Application\DeploymentFailed;
use App\Notifications\Application\DeploymentSuccess;
use App\Notifications\Application\StatusChanged;
@@ -19,10 +21,12 @@ use Exception;
use Illuminate\Console\Command;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Process;
use Mail;
use Str;
use function Laravel\Prompts\select;
use function Laravel\Prompts\text;
class TestEmail extends Command
{
@@ -44,9 +48,10 @@ class TestEmail extends Command
* Execute the console command.
*/
private ?MailMessage $mail = null;
private string $email = 'andras.bacsai@protonmail.com';
public function handle()
{
$email = select(
$type = select(
'Which Email should be sent?',
options: [
'emails-test' => 'Test',
@@ -60,15 +65,15 @@ class TestEmail extends Command
'waitlist-confirmation' => 'Waitlist Confirmation',
],
);
$type = set_transanctional_email_settings();
if (!$type) {
throw new Exception('No email settings found.');
}
$this->email = text('Email Address to send to');
set_transanctional_email_settings();
$this->mail = new MailMessage();
$this->mail->subject("Test Email");
switch ($email) {
switch ($type) {
case 'emails-test':
$this->mail = (new Test())->toMail();
$this->sendEmail();
break;
case 'application-deployment-success':
$application = Application::all()->first();
@@ -146,23 +151,20 @@ class TestEmail extends Command
case 'waitlist-invitation-link':
$this->mail = new MailMessage();
$this->mail->view('emails.waitlist-invitation', [
'email' => 'test2@example.com',
'password' => "supersecretpassword",
'loginLink' => 'https://coolify.io',
]);
$this->mail->subject('Congratulations! You are invited to join Coolify Cloud.');
$this->sendEmail();
break;
case 'waitlist-confirmation':
$this->mail = new MailMessage();
$this->mail->view(
'emails.waitlist-confirmation',
[
'confirmation_url' => 'http://example.com',
'cancel_url' => 'http://example.com',
]
);
$this->mail->subject('You are on the waitlist!');
$this->sendEmail();
$found = Waitlist::where('email', $this->email)->first();
if ($found) {
SendConfirmationForWaitlistJob::dispatch($this->email, $found->uuid);
} else {
throw new Exception('Waitlist not found');
}
break;
}
}
@@ -172,11 +174,7 @@ class TestEmail extends Command
[],
[],
fn (Message $message) => $message
->from(
'internal@example.com',
'Test Email',
)
->to('test@example.com')
->to($this->email)
->subject($this->mail->subject)
->html((string)$this->mail->render())
);

View File

@@ -91,8 +91,6 @@ class WaitlistInvite extends Command
$loginLink = route('auth.link', ['token' => $token]);
$mail = new MailMessage();
$mail->view('emails.waitlist-invitation', [
'email' => $this->next_patient->email,
'password' => $this->password,
'loginLink' => $loginLink,
]);
$mail->subject('Congratulations! You are invited to join Coolify Cloud.');

View File

@@ -74,6 +74,11 @@ class Kernel extends ConsoleKernel
if (!$scheduled_backup->enabled) {
continue;
}
if (is_null(data_get($scheduled_backup,'database'))) {
ray('database not found');
$scheduled_backup->delete();
continue;
}
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];

View File

@@ -6,6 +6,7 @@ use App\Actions\Server\InstallDocker;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Support\Collection;
use Livewire\Component;
@@ -70,9 +71,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
}
public function skipBoarding()
{
currentTeam()->update([
Team::find(currentTeam()->id)->update([
'show_boarding' => false
]);
ray(currentTeam());
refreshSession();
return redirect()->route('dashboard');
}
@@ -133,7 +135,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
{
$this->selectedExistingPrivateKey = null;
$this->privateKeyType = $type;
if ($type === 'create' && !isDev()) {
if ($type === 'create') {
$this->createNewPrivateKey();
}
$this->currentState = 'create-private-key';

View File

@@ -107,7 +107,14 @@ class EmailSettings extends Component
return general_error_handler($e, $this);
}
}
public function saveModel()
{
$this->team->save();
if (is_a($this->team, Team::class)) {
refreshSession();
}
$this->emit('success', 'Settings saved.');
}
public function submit()
{
try {

View File

@@ -17,6 +17,10 @@ class TelegramSettings extends Component
'team.telegram_notifications_deployments' => 'nullable|boolean',
'team.telegram_notifications_status_changes' => 'nullable|boolean',
'team.telegram_notifications_database_backups' => 'nullable|boolean',
'team.telegram_notifications_test_message_thread_id' => 'nullable|string',
'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string',
'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string',
'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string',
];
protected $validationAttributes = [
'team.telegram_token' => 'Token',

View File

@@ -8,7 +8,7 @@ use Livewire\Component;
class Change extends Component
{
public PrivateKey $private_key;
public $public_key;
protected $rules = [
'private_key.name' => 'required|string',
'private_key.description' => 'nullable|string',
@@ -21,6 +21,14 @@ class Change extends Component
'private_key.private_key' => 'private key'
];
public function mount()
{
try {
$this->public_key = $this->private_key->publicKey();
}catch(\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
}
public function delete()
{
try {

View File

@@ -37,7 +37,7 @@ class Create extends Component
if ($this->from === 'server') {
return redirect()->route('server.create');
}
return redirect()->route('private-key.show', ['private_key_uuid' => $private_key->uuid]);
return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]);
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}

View File

@@ -42,8 +42,13 @@ class Heading extends Component
["docker rm -f {$this->database->uuid}"],
$this->database->destination->server
);
if ($this->database->is_public) {
stopPostgresProxy($this->database);
$this->database->is_public = false;
}
$this->database->status = 'stopped';
$this->database->save();
$this->emit('refresh');
// $this->database->environment->project->team->notify(new StatusChanged($this->database));
}

View File

@@ -12,6 +12,7 @@ class General extends Component
public StandalonePostgresql $database;
public string $new_filename;
public string $new_content;
public string $db_url;
protected $listeners = ['refresh', 'save_init_script', 'delete_init_script'];
@@ -26,6 +27,8 @@ class General extends Component
'database.init_scripts' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
];
protected $validationAttributes = [
'database.name' => 'Name',
@@ -38,8 +41,44 @@ class General extends Component
'database.init_scripts' => 'Init Scripts',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function mount()
{
$this->getDbUrl();
}
public function getDbUrl() {
if ($this->database->is_public) {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->ip}:{$this->database->public_port}/{$this->database->postgres_db}";
} else {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}";
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...');
startPostgresProxy($this->database);
$this->emit('success', 'Database is now publicly accessible.');
} else {
stopPostgresProxy($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->getDbUrl();
$this->database->save();
} catch(Exception $e) {
$this->database->is_public = !$this->database->is_public;
return general_error_handler(err: $e, that: $this);
}
}
public function save_init_script($script)
{
$this->database->init_scripts = filter($this->database->init_scripts, fn ($s) => $s['filename'] !== $script['filename']);

View File

@@ -5,21 +5,84 @@ namespace App\Http\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Str;
class All extends Component
{
public $resource;
public bool $showPreview = false;
public string|null $modalId = null;
public ?string $variables = null;
public ?string $variablesPreview = null;
public string $view = 'normal';
protected $listeners = ['refreshEnvs', 'submit'];
public function mount()
{
$resourceClass = get_class($this->resource);
$resourceWithPreviews = ['App\Models\Application'];
$simpleDockerfile = !is_null(data_get($this->resource, 'dockerfile'));
if (Str::of($resourceClass)->contains($resourceWithPreviews) && !$simpleDockerfile) {
$this->showPreview = true;
}
$this->modalId = new Cuid2(7);
$this->getDevView();
}
public function getDevView()
{
$this->variables = $this->resource->environment_variables->map(function ($item) {
return "$item->key=$item->value";
})->sort()->join('
');
if ($this->showPreview) {
$this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) {
return "$item->key=$item->value";
})->sort()->join('
');
}
}
public function switch()
{
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
}
public function saveVariables($isPreview)
{
if ($isPreview) {
$variables = parseEnvFormatToArray($this->variablesPreview);
$existingVariables = $this->resource->environment_variables_preview();
$this->resource->environment_variables_preview()->delete();
} else {
$variables = parseEnvFormatToArray($this->variables);
$existingVariables = $this->resource->environment_variables();
$this->resource->environment_variables()->delete();
}
foreach ($variables as $key => $variable) {
$found = $existingVariables->where('key', $key)->first();
if ($found) {
$found->value = $variable;
$found->save();
continue;
} else {
$environment = new EnvironmentVariable();
$environment->key = $key;
$environment->value = $variable;
$environment->is_build_time = false;
$environment->is_preview = $isPreview ? true : false;
if ($this->resource->type() === 'application') {
$environment->application_id = $this->resource->id;
}
if ($this->resource->type() === 'standalone-postgresql') {
$environment->standalone_postgresql_id = $this->resource->id;
}
$environment->save();
}
}
$this->refreshEnvs();
}
public function refreshEnvs()
{
$this->resource->refresh();
$this->getDevView();
}
public function submit($data)
@@ -43,7 +106,7 @@ class All extends Component
$environment->standalone_postgresql_id = $this->resource->id;
}
$environment->save();
$this->resource->refresh();
$this->refreshEnvs();
$this->emit('success', 'Environment variable added successfully.');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);

View File

@@ -54,13 +54,19 @@ class Form extends Component
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server);
if ($uptime) {
$this->uptime = $uptime;
$this->emit('success', 'Server is reachable!');
} else {
throw new \Exception('Server is not rachable');
}
if ($dockerVersion) {
$this->dockerVersion = $dockerVersion;
$this->emit('proxyStatusUpdated');
$this->emit('success', 'Docker Engine 23+ is installed!');
} else {
throw new \Exception('Old Docker version detected (lower than 23).');
}
} catch (\Exception $e) {
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
return general_error_handler($e, that: $this);
}
}
@@ -77,8 +83,6 @@ class Form extends Component
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
}
public function submit()
{

View File

@@ -4,7 +4,6 @@ namespace App\Http\Livewire\Server;
use App\Models\Server;
use Livewire\Component;
use Masmerise\Toaster\Toaster;
class ShowPrivateKey extends Component
{
@@ -12,14 +11,24 @@ class ShowPrivateKey extends Component
public $privateKeys;
public $parameters;
public function setPrivateKey($private_key_id)
public function setPrivateKey($newPrivateKeyId)
{
$this->server->update([
'private_key_id' => $private_key_id
]);
refresh_server_connection($this->server->privateKey);
$this->server->refresh();
$this->checkConnection();
try {
$oldPrivateKeyId = $this->server->private_key_id;
$this->server->update([
'private_key_id' => $newPrivateKeyId
]);
$this->server->refresh();
refresh_server_connection($this->server->privateKey);
$this->checkConnection();
} catch (\Exception $e) {
$this->server->update([
'private_key_id' => $oldPrivateKeyId
]);
$this->server->refresh();
refresh_server_connection($this->server->privateKey);
return general_error_handler($e, that: $this);
}
}
public function checkConnection()
@@ -27,13 +36,17 @@ class ShowPrivateKey extends Component
try {
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server);
if ($uptime) {
Toaster::success('Server is reachable with this private key.');
$this->emit('success', 'Server is reachable with this private key.');
} else {
throw new \Exception('Server is not reachable with this private key.');
}
if ($dockerVersion) {
Toaster::success('Server is usable for Coolify.');
$this->emit('success', 'Server is usable for Coolify.');
} else {
throw new \Exception('Old Docker version detected (lower than 23).');
}
} catch (\Exception $e) {
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
throw new \Exception($e->getMessage());
}
}

View File

@@ -60,7 +60,6 @@ class Email extends Component
$this->validate([
'settings.resend_api_key' => 'required'
]);
$this->settings->smtp_enabled = false;
$this->settings->save();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) {
@@ -68,9 +67,18 @@ class Email extends Component
return general_error_handler($e, $this);
}
}
public function instantSaveResend() {
try {
$this->settings->smtp_enabled = false;
$this->submitResend();
} catch (\Exception $e) {
return general_error_handler($e, $this);
}
}
public function instantSave()
{
try {
$this->settings->resend_enabled = false;
$this->submit();
} catch (\Exception $e) {
return general_error_handler($e, $this);
@@ -89,7 +97,6 @@ class Email extends Component
'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) {

View File

@@ -23,7 +23,7 @@ class SwitchTeam extends Component
if (!$team_to_switch_to) {
return;
}
session(['currentTeam' => $team_to_switch_to]);
refreshSession($team_to_switch_to);
return redirect(request()->header('Referer'));
}
}

View File

@@ -10,6 +10,7 @@ use Livewire\Component;
class Index extends Component
{
public string $email;
public int $users = 0;
public int $waitingInLine = 0;
protected $rules = [
@@ -22,6 +23,7 @@ class Index extends Component
public function mount()
{
$this->waitingInLine = Waitlist::whereVerified(true)->count();
$this->users = User::count();
if (isDev()) {
$this->email = 'waitlist@example.com';
}

View File

@@ -27,7 +27,7 @@ class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique
public function handle(): void
{
try {
$this->cleanup_waitlist();
// $this->cleanup_waitlist();
} catch (\Exception $e) {
send_internal_notification('CleanupInstanceStuffsJob failed with error: ' . $e->getMessage());
ray($e->getMessage());

View File

@@ -24,31 +24,27 @@ class DatabaseBackupJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public Team|null $team = null;
public ?Team $team = null;
public Server $server;
public ScheduledDatabaseBackup|null $backup;
public string $database_type;
public ScheduledDatabaseBackup $backup;
public StandalonePostgresql $database;
public string $database_status;
public string|null $container_name = null;
public ScheduledDatabaseBackupExecution|null $backup_log = null;
public ?string $container_name = null;
public ?ScheduledDatabaseBackupExecution $backup_log = null;
public string $backup_status;
public string|null $backup_location = null;
public ?string $backup_location = null;
public string $backup_dir;
public string $backup_file;
public int $size = 0;
public string|null $backup_output = null;
public S3Storage|null $s3 = null;
public ?string $backup_output = null;
public ?S3Storage $s3 = null;
public function __construct($backup)
{
$this->backup = $backup;
$this->team = Team::find($backup->team_id);
$this->database = $this->backup->database;
$this->database_type = $this->database->type();
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->destination->server;
$this->database_status = $this->database->status;
$this->s3 = $this->backup->s3;
}
@@ -65,7 +61,7 @@ class DatabaseBackupJob implements ShouldQueue
public function handle(): void
{
try {
if ($this->database_status !== 'running') {
if (data_get($this->database, 'status') !== 'running') {
ray('database not running');
return;
}
@@ -84,7 +80,7 @@ class DatabaseBackupJob implements ShouldQueue
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
if ($this->database_type === 'standalone-postgresql') {
if ($this->database->type() === 'standalone-postgresql') {
$this->backup_standalone_postgresql();
}
$this->calculate_size();
@@ -99,7 +95,6 @@ class DatabaseBackupJob implements ShouldQueue
send_internal_notification('DatabaseBackupJob failed with: ' . $th->getMessage());
throw $th;
}
}
private function backup_standalone_postgresql(): void

View File

@@ -25,10 +25,8 @@ class SendConfirmationForWaitlistJob implements ShouldQueue
{
try {
$mail = new MailMessage();
$confirmation_url = base_url() . '/webhooks/waitlist/confirm?email=' . $this->email . '&confirmation_code=' . $this->uuid;
$cancel_url = base_url() . '/webhooks/waitlist/cancel?email=' . $this->email . '&confirmation_code=' . $this->uuid;
$mail->view('emails.waitlist-confirmation',
[
'confirmation_url' => $confirmation_url,
@@ -37,7 +35,7 @@ class SendConfirmationForWaitlistJob implements ShouldQueue
$mail->subject('You are on the waitlist!');
send_user_an_email($mail, $this->email);
} catch (\Throwable $th) {
send_internal_notification("SendConfirmationForWaitlistJob failed for {$mail} with error: " . $th->getMessage());
send_internal_notification("SendConfirmationForWaitlistJob failed for {$this->email} with error: " . $th->getMessage());
ray($th->getMessage());
throw $th;
}

View File

@@ -31,6 +31,7 @@ class SendMessageToTelegramJob implements ShouldQueue
public array $buttons,
public string $token,
public string $chatId,
public ?string $topicId = null,
) {
}
@@ -63,7 +64,9 @@ class SendMessageToTelegramJob implements ShouldQueue
'chat_id' => $this->chatId,
'text' => $this->text,
];
ray($payload);
if ($this->topicId) {
$payload['message_thread_id'] = $this->topicId;
}
$response = Http::post($url, $payload);
if ($response->failed()) {
throw new \Exception('Telegram notification failed with ' . $response->status() . ' status code.' . $response->body());

View File

@@ -105,7 +105,7 @@ class Application extends BaseModel
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false);
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->orderBy('key', 'asc');
}
public function runtime_environment_variables(): HasMany
@@ -127,7 +127,7 @@ class Application extends BaseModel
public function environment_variables_preview(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true);
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc');
}
public function runtime_environment_variables_preview(): HasMany

View File

@@ -20,13 +20,16 @@ class EnvironmentVariable extends Model
{
static::created(function ($environment_variable) {
if ($environment_variable->application_id && !$environment_variable->is_preview) {
ModelsEnvironmentVariable::create([
'key' => $environment_variable->key,
'value' => $environment_variable->value,
'is_build_time' => $environment_variable->is_build_time,
'application_id' => $environment_variable->application_id,
'is_preview' => true,
]);
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview',true)->first();
if (!$found) {
ModelsEnvironmentVariable::create([
'key' => $environment_variable->key,
'value' => $environment_variable->value,
'is_build_time' => $environment_variable->is_build_time,
'application_id' => $environment_variable->application_id,
'is_preview' => true,
]);
}
}
});
}

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use phpseclib3\Crypt\PublicKeyLoader;
class PrivateKey extends BaseModel
{
@@ -19,6 +20,15 @@ class PrivateKey extends BaseModel
return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all());
}
public function publicKey()
{
try {
return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH',['comment' => '']);
} catch (\Exception $e) {
return 'Error loading private key';
}
}
public function isEmpty()
{
if ($this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0) {

View File

@@ -23,6 +23,6 @@ class StandaloneDocker extends BaseModel
public function attachedTo()
{
return $this->applications->count() > 0 || $this->databases->count() > 0;
return $this->applications?->count() > 0 || $this->databases?->count() > 0;
}
}

View File

@@ -28,6 +28,11 @@ class StandalonePostgresql extends BaseModel
'is_readonly' => true
]);
});
static::deleted(function ($database) {
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();
instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false);
});
}
public function portsMappings(): Attribute

View File

@@ -28,7 +28,7 @@ class Team extends Model implements SendsDiscord, SendsEmail
{
return [
"token" => data_get($this, 'telegram_token', null),
"chat_id" => data_get($this, 'telegram_chat_id', null)
"chat_id" => data_get($this, 'telegram_chat_id', null),
];
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Notifications\Channels\SendsEmail;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
use Cache;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@@ -94,7 +95,9 @@ class User extends Authenticatable implements SendsEmail
public function currentTeam()
{
return Team::find(session('currentTeam')->id);
return Cache::remember('team:' . auth()->user()->id, 3600, function() {
return Team::find(session('currentTeam')->id);
});
}
public function otherTeams()

View File

@@ -16,6 +16,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public Application $application;
public string $deployment_uuid;
public ?ApplicationPreview $preview = null;

View File

@@ -16,6 +16,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public Application $application;
public string $deployment_uuid;
public ApplicationPreview|null $preview = null;

View File

@@ -14,6 +14,7 @@ class StatusChanged extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public $application;
public string $application_name;

View File

@@ -10,16 +10,30 @@ class TelegramChannel
{
$data = $notification->toTelegram($notifiable);
$telegramData = $notifiable->routeNotificationForTelegram();
$message = data_get($data, 'message');
$buttons = data_get($data, 'buttons', []);
ray($message, $buttons);
$telegramToken = data_get($telegramData, 'token');
$chatId = data_get($telegramData, 'chat_id');
$topicId = null;
$topicsInstance = get_class($notification);
if (!$telegramToken || !$chatId || !$message) {
throw new \Exception('Telegram token, chat id and message are required');
switch ($topicsInstance) {
case 'App\Notifications\StatusChange':
$topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id');
break;
case 'App\Notifications\Test':
$topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id');
break;
case 'App\Notifications\Deployment':
$topicId = data_get($notifiable, 'telegram_notifications_deployments_message_thread_id');
break;
case 'App\Notifications\DatabaseBackup':
$topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id');
break;
}
dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId));
if (!$telegramToken || !$chatId || !$message) {
return;
}
dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId));
}
}

View File

@@ -14,6 +14,7 @@ class BackupFailed extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public string $name;
public string $frequency;

View File

@@ -14,6 +14,7 @@ class BackupSuccess extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public string $name;
public string $frequency;

View File

@@ -12,13 +12,24 @@ class GeneralNotification extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public function __construct(public string $message)
{
}
public function via(object $notifiable): array
{
return [TelegramChannel::class, DiscordChannel::class];
$channels = [];
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
return $channels;
}
public function toDiscord(): string

View File

@@ -15,6 +15,7 @@ class NotReachable extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public function __construct(public Server $server)
{

View File

@@ -14,6 +14,7 @@ class Test extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public function __construct(public string|null $emails = null)
{
}
@@ -45,7 +46,7 @@ class Test extends Notification implements ShouldQueue
"buttons" => [
[
"text" => "Go to your dashboard",
"url" => 'https://coolify.io'
"url" => base_url()
]
],
];

View File

@@ -15,6 +15,7 @@ class InvitationLink extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public function via(): array
{
return [TransactionalEmailChannel::class];

View File

@@ -12,6 +12,7 @@ class Test extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 5;
public function __construct(public string $emails)
{
}

View File

@@ -16,6 +16,7 @@ class Modal extends Component
public string|null $modalTitle = null,
public string|null $modalBody = null,
public string|null $modalSubmit = null,
public bool $noSubmit = false,
public bool $yesOrNo = false,
public string $action = 'delete'
) {

View File

@@ -20,7 +20,7 @@ function create_standalone_postgresql($environment_id, $destination_uuid): Stand
}
return StandalonePostgresql::create([
'name' => generate_database_name('postgresql'),
'postgres_password' => \Illuminate\Support\Str::password(),
'postgres_password' => \Illuminate\Support\Str::password(symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),

View File

@@ -1,6 +1,7 @@
<?php
use App\Models\Server;
use App\Models\StandalonePostgresql;
use Symfony\Component\Yaml\Yaml;
function get_proxy_path()
@@ -166,3 +167,75 @@ function setup_default_redirect_404(string|null $redirect_url, Server $server)
}
}
}
function startPostgresProxy(StandalonePostgresql $database)
{
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
events {
worker_connections 1024;
}
stream {
server {
listen $database->public_port;
proxy_pass $database->uuid:5432;
}
}
EOF;
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'image' => "nginx:stable-alpine",
'container_name' => $containerName,
'restart' => RESTART_MODE,
'volumes' => [
"$configuration_dir/nginx.conf:/etc/nginx/nginx.conf:ro",
],
'ports' => [
"$database->public_port:$database->public_port",
],
'networks' => [
$database->destination->network,
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'stat /etc/nginx/nginx.conf || exit 1',
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 3,
'start_period' => '1s'
],
]
],
'networks' => [
$database->destination->network => [
'external' => true,
'name' => $database->destination->network,
'attachable' => true,
]
]
];
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
$nginxconf_base64 = base64_encode($nginxconf);
instant_remote_process([
"mkdir -p $configuration_dir",
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
"docker compose --project-directory {$configuration_dir} up -d >/dev/null",
], $database->destination->server);
}
function stopPostgresProxy(StandalonePostgresql $database)
{
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
}

View File

@@ -164,9 +164,9 @@ function refresh_server_connection(PrivateKey $private_key)
// Delete the old ssh mux file to force a new one to be created
Storage::disk('ssh-mux')->delete($server->muxFilename());
// check if user is authenticated
if (currentTeam()->id) {
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
}
// if (currentTeam()->id) {
// currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
// }
}
}
@@ -184,7 +184,7 @@ function validateServer(Server $server)
}
$server->settings->is_reachable = true;
$dockerVersion = instant_remote_process(['docker version|head -2|grep -i version'], $server, false);
$dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $server, false);
if (!$dockerVersion) {
$dockerVersion = null;
return [
@@ -192,7 +192,13 @@ function validateServer(Server $server)
"dockerVersion" => null,
];
}
$server->settings->is_usable = true;
$majorDockerVersion = Str::of($dockerVersion)->before('.')->value();
if ($majorDockerVersion <= 22) {
$dockerVersion = null;
$server->settings->is_usable = false;
} else {
$server->settings->is_usable = true;
}
return [
"uptime" => $uptime,
"dockerVersion" => $dockerVersion,
@@ -202,7 +208,7 @@ function validateServer(Server $server)
$server->settings->is_usable = false;
throw $e;
} finally {
$server->settings->save();
if(data_get($server,'settings')) $server->settings->save();
}
}

View File

@@ -28,6 +28,10 @@ function database_configuration_dir(): string
{
return '/data/coolify/databases';
}
function database_proxy_dir($uuid): string
{
return "/data/coolify/databases/$uuid/proxy";
}
function backup_dir(): string
{
@@ -53,9 +57,15 @@ function showBoarding(): bool
{
return currentTeam()->show_boarding ?? false;
}
function refreshSession(): void
function refreshSession(?Team $team = null): void
{
$team = Team::find(currentTeam()->id);
if (!$team) {
$team = Team::find(currentTeam()->id);
}
Cache::forget('team:' . auth()->user()->id);
Cache::remember('team:' . auth()->user()->id, 3600, function() use ($team) {
return $team;
});
session(['currentTeam' => $team]);
}
function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed
@@ -292,3 +302,25 @@ function setNotificationChannels($notifiable, $event)
}
return $channels;
}
function parseEnvFormatToArray($env_file_contents) {
$env_array = array();
$lines = explode("\n", $env_file_contents);
foreach ($lines as $line) {
if ($line === '' || substr($line, 0, 1) === '#') {
continue;
}
$equals_pos = strpos($line, '=');
if ($equals_pos !== false) {
$key = substr($line, 0, $equals_pos);
$value = substr($line, $equals_pos + 1);
if (substr($value, 0, 1) === '"' && substr($value, -1) === '"') {
$value = substr($value, 1, -1);
}
elseif (substr($value, 0, 1) === "'" && substr($value, -1) === "'") {
$value = substr($value, 1, -1);
}
$env_array[$key] = $value;
}
}
return $env_array;
}

View File

@@ -3,11 +3,11 @@
return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => 'https://abe219b6573947128ecf523c835f5f38@o1082494.ingest.sentry.io/4505347448045568',
'dsn' => 'https://62de992090e4e0cb28f18231835ea006@o1082494.ingest.sentry.io/4505347448045568',
// 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' => '4.0.0-beta.24',
'release' => '4.0.0-beta.33',
'server_name' => env('APP_ID', 'coolify'),
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.24';
return '4.0.0-beta.33';

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->text('telegram_notifications_test_message_thread_id')->nullable();
$table->text('telegram_notifications_deployments_message_thread_id')->nullable();
$table->text('telegram_notifications_status_changes_message_thread_id')->nullable();
$table->text('telegram_notifications_database_backups_message_thread_id')->nullable();
});
}
public function down(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->dropColumn('telegram_message_thread_id');
$table->dropColumn('telegram_notifications_test_message_thread_id');
$table->dropColumn('telegram_notifications_deployments_message_thread_id');
$table->dropColumn('telegram_notifications_status_changes_message_thread_id');
$table->dropColumn('telegram_notifications_database_backups_message_thread_id');
});
}
};

View File

@@ -8,13 +8,14 @@
<div class="w-96">
<form action="/user/confirm-password" method="POST" class="flex flex-col gap-2">
@csrf
<x-forms.input required type="password" name="password" label="{{ __('input.password') }}"
autofocus />
<x-forms.input required type="password" name="password" label="{{ __('input.password') }}" autofocus />
<x-forms.button type="submit">{{ __('auth.confirm_password') }}</x-forms.button>
</form>
@if ($errors->any())
<div class="text-center text-error">
<span>{{ __('auth.failed') }}</span>
<div class="text-xs text-center text-error">
@foreach ($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif
@if (session('status'))

View File

@@ -28,7 +28,9 @@
@endif
@if ($errors->any())
<div class="text-xs text-center text-error">
<span>{{ __('auth.failed') }}</span>
@foreach ($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif
@if (session('status'))

View File

@@ -43,7 +43,9 @@
@endif
@if ($errors->any())
<div class="text-xs text-center text-error">
<span>{{ __('auth.failed') }}</span>
@foreach ($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif
@if (session('status'))

View File

@@ -38,7 +38,9 @@
</form>
@if ($errors->any())
<div class="text-xs text-center text-error">
<span>{{ __('auth.failed') }}</span>
@foreach ($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif
</div>

View File

@@ -24,8 +24,10 @@
<x-forms.button type="submit">{{ __('auth.reset_password') }}</x-forms.button>
</form>
@if ($errors->any())
<div class="text-center text-error">
<span>{{ __('auth.failed') }}</span>
<div class="text-xs text-center text-error">
@foreach ($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif
@if (session('status'))

View File

@@ -20,7 +20,7 @@
</template>
<template x-if="showRecovery">
<div>
<x-forms.input required type="text" name="recovery_code "
<x-forms.input required type="text" name="recovery_code"
label="{{ __('input.recovery_code') }}" />
<div class="pt-2 text-xs cursor-pointer hover:underline hover:text-white"
x-on:click="showRecovery = !showRecovery">Use
@@ -31,8 +31,10 @@
<x-forms.button type="submit">{{ __('auth.login') }}</x-forms.button>
</form>
@if ($errors->any())
<div class="text-center text-error">
<span>{{ __('auth.failed') }}</span>
<div class="text-xs text-center text-error">
@foreach ($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif
@if (session('status'))

View File

@@ -24,7 +24,7 @@
@endif
</span>
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($instantSave) wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
@if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
wire:model.defer={{ $id }} @else wire:model.defer={{ $value ?? $id }} @endif />
</label>
</div>

View File

@@ -1,6 +1,6 @@
<dialog id="{{ $modalId }}" class="modal">
@if ($yesOrNo)
<form method="dialog" class="rounded modal-box" wire:submit.prevent='submit'>
<form method="dialog" class="rounded modal-box" @if(!$noSubmit) wire:submit.prevent='submit' @endif>
<div class="flex items-start">
<div class="flex items-center justify-center flex-shrink-0 w-10 h-10 mr-4 rounded-full">
<svg class="w-8 h-8 text-error" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
@@ -34,7 +34,7 @@
</form>
@else
<form method="dialog" class="flex flex-col w-11/12 max-w-5xl gap-2 rounded modal-box"
wire:submit.prevent='submit'>
@if(!$noSubmit) wire:submit.prevent='submit' @endif>
@isset($modalTitle)
<h3 class="text-lg font-bold">{{ $modalTitle }}</h3>
@endisset

View File

@@ -114,10 +114,10 @@
label="Description" id="privateKeyDescription" />
<x-forms.textarea required placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" label="Private Key"
id="privateKey" />
@if ($privateKeyType === 'create' && !isDev())
<span class="font-bold text-warning">Copy this to your server's ~/.ssh/authorized_keys
file.</span>
<x-forms.textarea rows="7" readonly label="Public Key" id="publicKey" />
@if ($privateKeyType === 'create')
<x-forms.textarea rows="7" readonly label="Public Key" id="publicKey" />
<span class="font-bold text-warning">ACTION REQUIRED: Copy the 'Public Key' to your server's ~/.ssh/authorized_keys
file.</span>
@endif
<x-forms.button type="submit">Save</x-forms.button>
</form>

View File

@@ -20,20 +20,31 @@
id="team.discord_webhook_url" label="Webhook" />
</form>
@if (data_get($team, 'discord_enabled'))
<h3 class="mt-4">Subscribe to events</h3>
<h2 class="mt-4">Subscribe to events</h2>
<div class="w-64">
@if (isDev())
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_test" label="Test" />
<h3 class="mt-4">Test</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_test" label="Enabled" />
</div>
@endif
<h4 class="mt-4">General</h4>
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_status_changes"
label="Container Status Changes" />
<h4 class="mt-4">Applications</h4>
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_deployments"
label="Deployments" />
<h4 class="mt-4">Databases</h4>
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_database_backups"
label="Backup Statuses" />
<h3 class="mt-4">Container Status Changes</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_status_changes"
label="Enabled" />
</div>
<h3 class="mt-4">Application Deployments</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_deployments"
label="Enabled" />
</div>
<h3 class="mt-4">Backup Status</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_database_backups"
label="Enabled" />
</div>
</div>
@endif
</div>

View File

@@ -21,7 +21,7 @@
Copy from Instance Settings
</x-forms.button>
@endif
@if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings'))
@if (isEmailEnabled($team) && auth()->user()->isAdminFromSession())
<x-forms.button onclick="sendTestEmail.showModal()"
class="text-white normal-case btn btn-xs no-animation btn-primary">
Send Test Email
@@ -36,12 +36,11 @@
label="Use hosted email service" />
</div>
@else
<div class="pb-4 w-96">
<x-forms.checkbox disabled id="team.use_instance_email_settings"
label="Use hosted email service (Pro+ subscription required)" />
</div>
<div class="pb-4 w-96">
<x-forms.checkbox disabled id="team.use_instance_email_settings"
label="Use hosted email service (Pro+ subscription required)" />
</div>
@endif
<h3 class="pb-4">Custom Email Service</h3>
@if (!$team->use_instance_email_settings)
<form class="flex flex-col items-end gap-2 pb-4 xl:flex-row" wire:submit.prevent='submitFromFields'>
<x-forms.input required id="team.smtp_from_name" helper="Name used in emails." label="From Name" />
@@ -110,19 +109,27 @@
</div>
@endif
@if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings'))
<h3 class="mt-4">Subscribe to events</h3>
<h2 class="mt-4">Subscribe to events</h2>
<div class="w-64">
@if (isDev())
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_test" label="Test" />
<h3 class="mt-4">Test</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_test" label="Enabled" />
</div>
@endif
<h4 class="mt-4">General</h4>
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_status_changes"
label="Container Status Changes" />
<h4 class="mt-4">Applications</h4>
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_deployments" label="Deployments" />
<h4 class="mt-4">Databases</h4>
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_database_backups"
label="Backup Statuses" />
<h3 class="mt-4">Container Status Changes</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_status_changes" label="Enabled" />
</div>
<h3 class="mt-4">Application Deployments</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_deployments" label="Enabled" />
</div>
<h3 class="mt-4">Backup Status</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_database_backups"
label="Enabled" />
</div>
</div>
@endif
</div>

View File

@@ -16,27 +16,50 @@
<x-forms.checkbox instantSave id="team.telegram_enabled" label="Notification Enabled" />
</div>
<div class="flex gap-2">
<x-forms.input type="password" helper="Get it from the <a class='inline-block text-white underline' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram." required
id="team.telegram_token" label="Token" />
<x-forms.input type="password" helper="Recommended to add your bot to a group chat and add its Chat ID here." required
<x-forms.input type="password"
helper="Get it from the <a class='inline-block text-white underline' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram."
required id="team.telegram_token" label="Token" />
<x-forms.input
helper="Recommended to add your bot to a group chat and add its Chat ID here." required
id="team.telegram_chat_id" label="Chat ID" />
</div>
</form>
@if (data_get($team, 'telegram_enabled'))
<h3 class="mt-4">Subscribe to events</h3>
<div class="w-64">
<h2 class="mt-4">Subscribe to events</h2>
<div class="w-96">
@if (isDev())
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_test" label="Test" />
<h3 class="mt-4">Test</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_test" label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_test_message_thread_id" label="Custom Topic ID" />
</div>
@endif
<h4 class="mt-4">General</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_status_changes"
label="Container Status Changes" />
<h4 class="mt-4">Applications</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_deployments"
label="Deployments" />
<h4 class="mt-4">Databases</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_database_backups"
label="Backup Statuses" />
<h3 class="mt-4">Container Status Changes</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_status_changes"
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_status_changes_message_thread_id" label="Custom Topic ID" />
</div>
<h3 class="mt-4">Application Deployments</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_deployments"
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_deployments_message_thread_id" label="Custom Topic ID" />
</div>
<h3 class="mt-4">Backup Status</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_database_backups"
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_database_backups_message_thread_id" label="Custom Topic ID" />
</div>
</div>
@endif
@endif
</form>
</div>

View File

@@ -19,6 +19,10 @@
<x-forms.input id="private_key.name" label="Name" required />
<x-forms.input id="private_key.description" label="Description" />
<div>
<div class="flex items-end gap-2 py-2 ">
<div class="pl-1 ">Public Key</div>
</div>
<x-forms.input readonly id="public_key" />
<div class="flex items-end gap-2 py-2 ">
<div class="pl-1 ">Private Key <span class='text-helper'>*</span></div>
<div class="text-xs text-white underline cursor-pointer" x-cloak x-show="!showPrivateKey"
@@ -43,5 +47,6 @@
<x-forms.textarea rows="10" id="private_key.private_key" required />
</div>
</div>
</form>
</div>

View File

@@ -51,10 +51,15 @@
<x-forms.input label="Host Auth Method" id="database.postgres_host_auth_method"
placeholder="If empty, use default. See in docker docs." />
</div>
<div class="">
<div class="flex flex-col gap-2">
<h3 class="py-2">Network</h3>
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings"
<div class="flex items-end gap-2">
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold text-warning'>Example</span>3000:5432,3002:5433" />
<x-forms.input placeholder="5432" disabled="{{$database->is_public}}" id="database.public_port" label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div>
<x-forms.input label="Postgres URL" readonly wire:model="db_url" />
</div>
</form>
<div class="pb-16">

View File

@@ -76,6 +76,10 @@
</div>
</div> --}}
</div>
<h2 class="py-4">Services</h2>
<div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3">
Ghost, Plausible, Wordpress, etc... Coming very very soon...
</div>
@endif
@if ($current_step === 'servers')
<ul class="pb-10 steps">
@@ -135,10 +139,11 @@
</div>
@endif
@if ($current_step === 'existing-postgresql')
<form wire:submit.prevent='addExistingPostgresql' class="flex items-end gap-2">
<x-forms.input placeholder="postgres://username:password@database:5432" label="Database URL" id="existingPostgresqlUrl" />
<x-forms.button type="submit">Add Database</x-forms.button>
</form>
<form wire:submit.prevent='addExistingPostgresql' class="flex items-end gap-2">
<x-forms.input placeholder="postgres://username:password@database:5432" label="Database URL"
id="existingPostgresqlUrl" />
<x-forms.button type="submit">Add Database</x-forms.button>
</form>
@endif
</div>
</div>

View File

@@ -4,23 +4,40 @@
<h2>Environment Variables</h2>
<x-forms.button class="btn" onclick="newVariable.showModal()">+ Add</x-forms.button>
<livewire:project.shared.environment-variable.add />
<x-forms.button
wire:click='switch'>{{ $view === 'normal' ? 'Developer view' : 'Normal view' }}</x-forms.button>
</div>
<div>Environment (secrets) variables for this resource.</div>
<div>Environment variables (secrets) for this resource.</div>
</div>
@forelse ($resource->environment_variables as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" />
@empty
<div class="text-neutral-500">No environment variables found.</div>
@endforelse
@if ($resource->type() === 'application' && $resource->environment_variables_preview->count() > 0)
<div>
<h3>Preview Deployments</h3>
<div>Environment (secrets) variables for Preview Deployments.</div>
</div>
@foreach ($resource->environment_variables_preview as $env)
@if ($view === 'normal')
@forelse ($resource->environment_variables as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" />
@endforeach
@empty
<div class="text-neutral-500">No environment variables found.</div>
@endforelse
@if ($resource->type() === 'application' && $resource->environment_variables_preview->count() > 0 && $showPreview)
<div>
<h3>Preview Deployments</h3>
<div>Environment (secrets) variables for Preview Deployments.</div>
</div>
@foreach ($resource->environment_variables_preview as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" />
@endforeach
@endif
@else
<form wire:submit.prevent='saveVariables(false)' class="flex flex-col gap-2">
<x-forms.textarea rows=5 class="whitespace-pre-wrap" label="Environment Variables"
id="variables"></x-forms.textarea>
<x-forms.button type="submit" class="btn btn-primary">Save</x-forms.button>
</form>
@if ($showPreview)
<form wire:submit.prevent='saveVariables(true)' class="flex flex-col gap-2">
<x-forms.textarea rows=5 class="whitespace-pre-wrap" label="Preview Environment Variables"
id="variablesPreview"></x-forms.textarea>
<x-forms.button type="submit" class="btn btn-primary">Save</x-forms.button>
</form>
@endif
@endif
</div>

View File

@@ -18,11 +18,6 @@
@else
<x-forms.button type="submit">Save</x-forms.button>
@endif
@if (!$server->settings->is_reachable || !$server->settings->is_usable)
<x-forms.button wire:click.prevent='validateServer'>
Validate Server
</x-forms.button>
@endif
</div>
@if (!$server->settings->is_reachable || !$server->settings->is_usable)
@@ -51,43 +46,25 @@
</div>
</div>
</div>
@if ($server->settings->is_reachable)
@if (!$server->settings->is_reachable)
<x-forms.button class="mt-8 mb-4 box" wire:click.prevent='validateServer'>
Validate Server
</x-forms.button>
@endif
@if ($server->settings->is_reachable && !$server->settings->is_usable && $server->id !== 0)
<x-forms.button wire:poll.2000ms='validateServer' class="mt-8 mb-4 box" onclick="installDocker.showModal()" wire:click.prevent='installDocker' isHighlighted>
Install Docker Engine 24.0
</x-forms.button>
@endif
@if ($server->settings->is_usable)
<h3 class="py-4">Settings</h3>
<div class="flex items-center w-64 gap-2">
<x-forms.input id="cleanup_after_percentage" label="Disk Cleanup threshold (%)" required
helper="Disk cleanup job will be executed if disk usage is more than this number." />
</div>
<h3 class="py-4">Actions</h3>
<div class="flex items-center gap-2">
<x-forms.button wire:click.prevent='validateServer'>
Check Server Details
</x-forms.button>
@if ($server->id !== 0)
<x-forms.button wire:click.prevent='installDocker' isHighlighted>
@if ($server->settings->is_usable)
Reconfigure Docker Engine
@else
Install Docker Engine
@endif
</x-forms.button>
@endif
</div>
@endif
<div class="container w-full py-4 mx-auto">
<livewire:activity-monitor header="Logs" />
</div>
@isset($uptime)
<h3 class="pb-3">Server Info</h3>
<div class="py-2 pb-4">
<p>Uptime: {{ $uptime }}</p>
@isset($dockerVersion)
<p>Docker Engine {{ $dockerVersion }}</p>
@endisset
</div>
@endisset
</form>
<h2>Danger Zone</h2>
<h2 class="pt-4">Danger Zone</h2>
<div class="">Woah. I hope you know what are you doing.</div>
<h4 class="pt-4">Delete Server</h4>
<div class="pb-4">This will remove this server from Coolify. Beware! There is no coming

View File

@@ -22,11 +22,13 @@
@endif
</div>
<h3 class="pb-4">Select a different Private Key</h3>
<div class="grid gap-2">
<h3 class="pb-4">Choose another Key</h3>
<div class="grid grid-cols-3 gap-2">
@forelse ($privateKeys as $private_key)
<div class="cursor-pointer box" wire:click='setPrivateKey({{ $private_key->id }})'>{{ $private_key->name }}
</div>
<x-forms.button class="flex flex-col box" wire:click='setPrivateKey({{ $private_key->id }})'>
<div>{{ $private_key->name }}</div>
<div class="text-xs">{{ $private_key->description }}</div>
</x-forms.button>
@empty
<div>No private keys found.
<x-use-magic-bar link="/security/private-key/new" />

View File

@@ -1,4 +1,14 @@
<div>
<x-modal noSubmit modalId="installDocker">
<x-slot:modalBody>
<livewire:activity-monitor header="Installation Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="installDocker.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
<x-server.navbar :server="$server" />
<livewire:server.form :server="$server" />
</div>

View File

@@ -22,12 +22,12 @@
<x-forms.button type="submit">
Save
</x-forms.button>
@if ($settings->resend_enabled || $settings->smtp_enabled)
<x-forms.button onclick="sendTestEmail.showModal()"
class="text-white normal-case btn btn-xs no-animation btn-primary">
Send Test Email
</x-forms.button>
@endif
@if (isEmailEnabled($settings))
<x-forms.button onclick="sendTestEmail.showModal()"
class="text-white normal-case btn btn-xs no-animation btn-primary">
Send Test Email
</x-forms.button>
@endif
</div>
</form>
<div class="flex flex-col gap-4">
@@ -67,14 +67,15 @@
<summary class="text-xl collapse-title">
<div>Resend</div>
<div class="w-32">
<x-forms.checkbox instantSave='submitResend' id="settings.resend_enabled" label="Enabled" />
<x-forms.checkbox instantSave='instantSaveResend' id="settings.resend_enabled" label="Enabled" />
</div>
</summary>
<div class="collapse-content">
<form wire:submit.prevent='submitResend' class="flex flex-col">
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input type="password" id="settings.resend_api_key" placeholder="API key" label="Host" />
<x-forms.input type="password" id="settings.resend_api_key" placeholder="API key"
label="Host" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">

View File

@@ -23,7 +23,8 @@
<x-forms.input id="email" type="email" label="Email" placeholder="youareawesome@protonmail.com" />
<x-forms.button type="submit">Join Waitlist</x-forms.button>
</form>
Waiting in the line: <span class="font-bold text-warning">{{ $waitingInLine }}</span>
<div>People waiting in the line: <span class="font-bold text-warning">{{ $waitingInLine }}</div>
<div>Already using Coolify Cloud: <span class="font-bold text-warning">{{ $users }}</div>
<div class="pt-8">
This is a paid & hosted version of Coolify.<br> See the pricing <a href="https://coolify.io/pricing" class="text-warning">here</a>.
</div>

View File

@@ -22,7 +22,7 @@
class="relative flex duration-300 transform transition ease-in-out max-w-md w-full pointer-events-auto {{ $position->is('center') ? 'text-center' : 'text-left' }}"
:class="toast.select({ error: 'text-white', info: 'text-white', success: 'text-white', warning: 'text-white' })"
>
<i class=" flex items-center gap-2 select-none not-italic pr-6 pl-4 py-3 rounded shadow-lg w-full {{ $alignment->is('bottom') ? 'mt-3' : 'mb-3' }}"
<i class=" flex items-center gap-2 select-none not-italic pr-6 pl-4 py-3 rounded shadow-lg w-full {{ $alignment->is('bottom') ? 'mt-3' : 'mb-3' }}"
:class="toast.select({
error: 'bg-coolgray-300',
info: 'bg-coolgray-300',
@@ -71,7 +71,7 @@
</svg>
</div>
</template>
<span x-html="toast.message" />
<span x-html="toast.message" class="w-full pr-10 break-words" />
</i>
@if ($closeable)

View File

@@ -181,14 +181,24 @@ Route::get('/waitlist/confirm', function () {
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.expiration'))) {
$found->verified = true;
$found->save();
send_internal_notification('Waitlist confirmed: ' . $email);
return 'Thank you for confirming your email address. We will notify you when you are next in line.';
if ($found) {
if (!$found->verified) {
if ($found->created_at > now()->subMinutes(config('constants.waitlist.expiration'))) {
$found->verified = true;
$found->save();
send_internal_notification('Waitlist confirmed: ' . $email);
return 'Thank you for confirming your email address. We will notify you when you are next in line.';
} else {
$found->delete();
send_internal_notification('Waitlist expired: ' . $email);
return 'Your confirmation code has expired. Please sign up again.';
}
}
}
return redirect()->route('dashboard');
} catch (error) {
} catch (Exception $e) {
send_internal_notification('Waitlist confirmation failed: ' . $e->getMessage());
ray($e->getMessage());
return redirect()->route('dashboard');
}
})->name('webhooks.waitlist.confirm');
@@ -203,7 +213,9 @@ Route::get('/waitlist/cancel', function () {
return 'Your email address has been removed from the waitlist.';
}
return redirect()->route('dashboard');
} catch (error) {
} catch (Exception $e) {
send_internal_notification('Waitlist cancellation failed: ' . $e->getMessage());
ray($e->getMessage());
return redirect()->route('dashboard');
}
})->name('webhooks.waitlist.cancel');
@@ -225,7 +237,7 @@ Route::post('/payments/stripe/events', function () {
]);
$type = data_get($event, 'type');
$data = data_get($event, 'data.object');
ray('Event: '. $type);
ray('Event: ' . $type);
switch ($type) {
case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id');
@@ -263,13 +275,13 @@ Route::post('/payments/stripe/events', function () {
'stripe_invoice_paid' => true,
]);
break;
// case 'invoice.payment_failed':
// $customerId = data_get($data, 'customer');
// $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
// if ($subscription) {
// SubscriptionInvoiceFailedJob::dispatch($subscription->team);
// }
// break;
// case 'invoice.payment_failed':
// $customerId = data_get($data, 'customer');
// $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
// if ($subscription) {
// SubscriptionInvoiceFailedJob::dispatch($subscription->team);
// }
// break;
case 'customer.subscription.updated':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
@@ -287,9 +299,9 @@ Route::post('/payments/stripe/events', function () {
]);
ray($feedback, $comment, $alreadyCancelAtPeriodEnd, $cancelAtPeriodEnd);
if ($feedback) {
$reason = "Cancellation feedback for {$subscription->team->id}: '" . $feedback ."'";
$reason = "Cancellation feedback for {$subscription->team->id}: '" . $feedback . "'";
if ($comment) {
$reason .= ' with comment: \'' . $comment ."'";
$reason .= ' with comment: \'' . $comment . "'";
}
send_internal_notification($reason);
}
@@ -307,7 +319,7 @@ Route::post('/payments/stripe/events', function () {
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$subscription->update([
'stripe_subscription_id' => null,
'stripe_plan_id'=> null,
'stripe_plan_id' => null,
'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false,
]);

View File

@@ -4,7 +4,7 @@
"version": "3.12.36"
},
"v4": {
"version": "4.0.0-beta.24"
"version": "4.0.0-beta.33"
}
}
}