Compare commits

...

98 Commits

Author SHA1 Message Date
Andras Bacsai
4de4587ea6 Merge pull request #1196 from coollabsio/next
version++
2023-09-01 16:09:23 +02:00
Andras Bacsai
8675e1d13f version++ 2023-09-01 16:09:03 +02:00
Andras Bacsai
6ceacc68cc more unique container name 2023-09-01 16:07:46 +02:00
Andras Bacsai
4aacf134b7 Merge pull request #1195 from coollabsio/next
v4.0.0-beta.23
2023-09-01 16:03:28 +02:00
Andras Bacsai
0605772715 better emails 2023-09-01 15:55:55 +02:00
Andras Bacsai
3fa53556f4 better emails 2023-09-01 15:52:18 +02:00
Andras Bacsai
76510b8971 fix: button loading animation 2023-09-01 11:35:40 +02:00
Andras Bacsai
66162966b9 fix: sentry bug 2023-09-01 11:35:33 +02:00
Andras Bacsai
71e1571c39 Add affected users to sentry 2023-09-01 11:20:58 +02:00
Andras Bacsai
806b761e74 Merge pull request #1194 from coollabsio/next
Sentry version update
2023-09-01 10:33:19 +02:00
Andras Bacsai
0176b38958 oops 2023-09-01 10:32:29 +02:00
Andras Bacsai
7a180c7310 Merge pull request #1193 from coollabsio/next
v4.0.0-beta.21
2023-09-01 10:27:49 +02:00
Andras Bacsai
3e1120182c update 2023-09-01 10:21:35 +02:00
Andras Bacsai
8e86ce671c fix: dockerimage jobs are not overlapping 2023-09-01 10:11:00 +02:00
Andras Bacsai
f75a324030 fix: proxy start job 2023-09-01 10:02:24 +02:00
Andras Bacsai
3fabff93f6 fix a few things 2023-09-01 09:34:25 +02:00
Andras Bacsai
e74efc4e76 fix 2023-08-31 21:56:53 +02:00
Andras Bacsai
472ed0753d update 2023-08-31 21:51:05 +02:00
Andras Bacsai
67538ff60c no double slot 2023-08-31 21:46:30 +02:00
Andras Bacsai
ae8bd69106 able to use resend for pro+ users 2023-08-31 15:00:59 +02:00
Andras Bacsai
2538890b52 feat: add resend as transactional emails 2023-08-31 13:10:39 +02:00
Andras Bacsai
87dd819ae4 fix: password confirmation 2023-08-31 09:56:37 +02:00
Andras Bacsai
7ec560d4a2 update 2023-08-30 18:36:06 +02:00
Andras Bacsai
6f9cd6a16b no localhost in cloud 2023-08-30 18:35:20 +02:00
Andras Bacsai
923af88336 fix: subscriptions 2023-08-30 18:23:55 +02:00
Andras Bacsai
5b6667c461 refactor + fixes 2023-08-30 16:01:38 +02:00
Andras Bacsai
6f00740f67 better boarding flow 2023-08-30 14:46:51 +02:00
Andras Bacsai
248863cf16 update boarding process 2023-08-30 11:26:46 +02:00
Andras Bacsai
97d48823dd improve boarding 2023-08-30 11:06:44 +02:00
Andras Bacsai
5eb41e1a15 fix boarding 2023-08-29 20:34:01 +02:00
Andras Bacsai
4a4837d9f5 fix 2023-08-29 20:25:42 +02:00
Andras Bacsai
4ad72fab7b refactor 2023-08-29 16:31:46 +02:00
Andras Bacsai
fe68e45609 refactor 2023-08-29 15:51:30 +02:00
Andras Bacsai
291b9a84ef refactoring 2023-08-29 14:36:17 +02:00
Andras Bacsai
2f9b7b188a ui update 2023-08-29 10:11:18 +02:00
Andras Bacsai
d04d41bc23 update dockercleanupjob 2023-08-29 10:00:29 +02:00
Andras Bacsai
6cb3d7167f fix ui 2023-08-28 23:27:46 +02:00
Andras Bacsai
90b1659a18 fix 2023-08-28 22:25:18 +02:00
Andras Bacsai
1aaf44f9b0 fix 2023-08-28 21:22:53 +02:00
Andras Bacsai
d7cfb84351 fix gh create button 2023-08-28 21:15:30 +02:00
Andras Bacsai
d28cf0b76d fix: webhook endpoint in cloud and no system wide gh app 2023-08-28 21:03:07 +02:00
Andras Bacsai
b4a3236284 fix 2023-08-28 20:52:45 +02:00
Andras Bacsai
556168892d fix job 2023-08-28 20:45:53 +02:00
Andras Bacsai
77667be570 fix: validation 2023-08-28 20:29:44 +02:00
Andras Bacsai
f48a912287 update cloud no localhost server 2023-08-28 18:02:31 +02:00
Andras Bacsai
af30d0831d do not seed in coolify cloud 2023-08-28 15:03:44 +02:00
Andras Bacsai
5989eb8f6e rename license server 2023-08-28 15:02:03 +02:00
Andras Bacsai
2bb778834b remove cloud configs 2023-08-28 14:40:18 +02:00
Andras Bacsai
a5ce191e4d add example cloud 2023-08-28 13:33:20 +02:00
Andras Bacsai
7617756576 remove dynamic sentry version checker 2023-08-28 13:32:36 +02:00
Andras Bacsai
0dfd3a5b0e run scheduled jobs on one server 2023-08-28 11:43:01 +02:00
Andras Bacsai
61a54f48c5 update 2023-08-28 10:44:11 +02:00
Andras Bacsai
5ca0237e34 fix logging on ui 2023-08-27 22:05:37 +02:00
Andras Bacsai
ab1207e461 fix:dockerCleanupjob 2023-08-27 21:36:11 +02:00
Andras Bacsai
75fea4f7c0 disable docker cleanup job 2023-08-27 16:23:02 +02:00
Andras Bacsai
fb34eb5394 new version 2023-08-27 15:49:35 +02:00
Andras Bacsai
41101217c6 Merge pull request #1187 from coollabsio/next
v4.0.0.beta-21
2023-08-27 15:48:33 +02:00
Andras Bacsai
19b1f5004a fix 2023-08-27 15:44:36 +02:00
Andras Bacsai
75fcd88f73 do not show boarding flow yet 2023-08-27 15:28:48 +02:00
Andras Bacsai
c21ce45d70 show public key of generated private key 2023-08-27 15:23:47 +02:00
Andras Bacsai
9f10cb2899 updates 2023-08-27 15:08:53 +02:00
Andras Bacsai
aa0c621223 update ignores 2023-08-27 15:03:53 +02:00
Andras Bacsai
f1dfb9051c delete file 2023-08-27 15:03:26 +02:00
Andras Bacsai
932e58531d fix: fqdn on apps 2023-08-27 14:55:57 +02:00
Andras Bacsai
35a75e1066 disable stopped notification until fixed 2023-08-27 14:53:16 +02:00
Andras Bacsai
522713473d proxy status poll every 10 seconds 2023-08-25 16:11:41 +02:00
Andras Bacsai
3d6e268d15 potential fix: dockercleanuppjob 2023-08-25 13:04:21 +02:00
Andras Bacsai
cf8129dcbb add gh private repo add new gh app 2023-08-25 12:50:52 +02:00
Andras Bacsai
3985cca8cc add pure dockerfile in seeder 2023-08-25 12:40:53 +02:00
Andras Bacsai
86ab65ef66 updates 2023-08-25 12:13:22 +02:00
Andras Bacsai
9db9616a43 fix route subs 2023-08-25 09:53:40 +02:00
Andras Bacsai
39fd6f054b fix 2023-08-24 21:42:47 +02:00
Andras Bacsai
9b5f6ceca8 fix server validated 2023-08-24 21:38:18 +02:00
Andras Bacsai
746da1a76e temporary fix 2023-08-24 21:34:24 +02:00
Andras Bacsai
0a2d0da36f fix proxy check jo 2023-08-24 21:24:44 +02:00
Andras Bacsai
0076455e6e fix proxy job 2023-08-24 21:20:24 +02:00
Andras Bacsai
b674a0ed88 throw errors in jobs 2023-08-24 21:09:58 +02:00
Andras Bacsai
90dba34ecc fix 2023-08-24 21:04:17 +02:00
Andras Bacsai
01b40b26f5 do not send discord message if there is not url 2023-08-24 21:00:19 +02:00
Andras Bacsai
c0805a285e update 2023-08-24 20:58:51 +02:00
Andras Bacsai
ac76870d67 fix dbcontainerstatusjob 2023-08-24 20:52:43 +02:00
Andras Bacsai
7160f50322 update autoupdate job 2023-08-24 20:51:14 +02:00
Andras Bacsai
ba39f2595c update 2023-08-24 20:49:54 +02:00
Andras Bacsai
38688b7065 fix 2023-08-24 17:52:43 +02:00
Andras Bacsai
9ef3218bb5 updates 2023-08-24 17:41:11 +02:00
Andras Bacsai
6f14e127a3 fix things 2023-08-24 17:08:32 +02:00
Andras Bacsai
39890b319a add stripe subscription 2023-08-24 16:14:09 +02:00
Andras Bacsai
2d8f166e4a update a few things 2023-08-23 16:40:59 +02:00
Andras Bacsai
d62af76097 update boarding flow 2023-08-23 10:14:39 +02:00
Andras Bacsai
b39ca51d41 wip: boarding 2023-08-22 17:44:49 +02:00
Andras Bacsai
2414ddd360 remove seeders in dev 2023-08-21 20:19:47 +02:00
Andras Bacsai
bed959f1cd feat: rolling update 2023-08-21 18:00:12 +02:00
Andras Bacsai
a3f3470137 add resend as mailer 2023-08-21 11:11:51 +02:00
Andras Bacsai
b7ec1d7d65 fix: limits & server creation page 2023-08-21 10:18:11 +02:00
Andras Bacsai
7e37068fc0 send notification of autoupdate 2023-08-17 16:26:55 +02:00
Andras Bacsai
d049acad70 update compose for prod 2023-08-17 16:22:49 +02:00
Andras Bacsai
07044680d4 change secrets for bunny sync 2023-08-17 16:20:27 +02:00
Andras Bacsai
847b3fe54f feat: invite by email from waitlist 2023-08-17 16:12:08 +02:00
232 changed files with 4542 additions and 1911 deletions

View File

@@ -20,3 +20,5 @@ yarn-error.log
/.npm
/.bash_history
/_data
.rnd
/.ssh

View File

@@ -14,3 +14,9 @@ APP_URL=http://localhost
APP_PORT=8000
DUSK_DRIVER_URL=http://selenium:4444
## For Andras only
# To purge cache
BUNNY_API_KEY=
# To upload assets
BUNNY_STORAGE_API_KEY=

View File

@@ -1,6 +0,0 @@
# Secrets related to pushing to GH, Sync files to BunnyCDN etc. Only for maintainers.
# Not related to Coolify, but to how we publish new versions.
GITHUB_TOKEN=
BUNNY_API_KEY=
BUNNY_STORAGE_API_KEY=

2
.gitignore vendored
View File

@@ -29,3 +29,5 @@ _ide_helper.php
.gitignore
.phpstorm.meta.php
_ide_helper_models.php
.rnd
/.ssh

View File

@@ -17,7 +17,7 @@ class StartPostgresql
public function __invoke(Server $server, StandalonePostgresql $database)
{
$this->database = $database;
$container_name = generate_container_name($this->database->uuid);
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
@@ -36,7 +36,7 @@ class StartPostgresql
'image' => $this->database->image,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => 'always',
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],

View File

@@ -14,14 +14,14 @@ class CheckResaleLicense
$settings->update([
'is_resale_license_active' => false,
]);
if (is_dev()) {
if (isDev()) {
return;
}
if (!$settings->resale_license) {
return;
}
$base_url = config('coolify.license_url');
if (is_dev()) {
if (isDev()) {
$base_url = 'http://host.docker.internal:8787';
}
$instance_id = config('app.id');

View File

@@ -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);
}
}

View File

@@ -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'];

View File

@@ -12,29 +12,41 @@ class InstallDocker
{
$dockerVersion = '23.0';
$config = base64_encode('{ "live-restore": true }');
$activity = 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...",
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh",
"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...",
"systemctl restart docker",
"echo ####### Creating default network...",
"docker network create --attachable coolify",
"echo ####### Done!"
], $server);
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
'team_id' => $team->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...",
], $server);
} else {
$activity = 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...",
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh",
"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...",
"systemctl restart docker",
"echo ####### Creating default network...",
"docker network create --attachable coolify",
"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

@@ -7,9 +7,9 @@ use App\Models\Server;
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,43 +17,44 @@ class UpdateCoolify
$settings = InstanceSettings::get();
ray('Running InstanceAutoUpdateJob');
$localhost_name = 'localhost';
if (is_dev()) {
$localhost_name = 'testing-local-docker-container';
$this->server = Server::where('name', $localhost_name)->first();
if (!$this->server) {
return;
}
$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->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();
} else {
if (!$settings->is_auto_update_enabled) {
throw new \Exception('Auto update is disabled');
return 'Auto update is disabled';
}
if ($this->latest_version === $this->current_version) {
throw new \Exception('Already on latest version');
if ($this->latestVersion === $this->currentVersion) {
return 'Already on latest version';
}
if (version_compare($this->latest_version, $this->current_version, '<')) {
throw new \Exception('Latest version is lower than current version?!');
if (version_compare($this->latestVersion, $this->currentVersion, '<')) {
return 'Latest version is lower than current version?!';
}
$this->update();
}
return;
} catch (\Exception $e) {
send_internal_notification('InstanceAutoUpdateJob done to version: ' . $this->latestVersion . ' from version: ' . $this->currentVersion);
} catch (\Exception $th) {
ray('InstanceAutoUpdateJob failed');
ray($e->getMessage());
return;
ray($th->getMessage());
send_internal_notification('InstanceAutoUpdateJob failed: ' . $th->getMessage());
throw $th;
}
}
private function update()
{
if (is_dev()) {
ray("Running update on local docker container. Updating to $this->latest_version");
if (isDev()) {
ray("Running update on local docker container. Updating to $this->latestVersion");
remote_process([
"sleep 10"
], $this->server);
@@ -63,7 +64,7 @@ class UpdateCoolify
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;
}

View File

@@ -19,21 +19,29 @@ class InviteFromWaitlist extends Command
*
* @var string
*/
protected $signature = 'app:invite-from-waitlist';
protected $signature = 'app:invite-from-waitlist {email?}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send invitation to the next user in the waitlist';
protected $description = 'Send invitation to the next user (or by email) in the waitlist';
/**
* Execute the console command.
*/
public function handle()
{
$this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first();
if ($this->argument('email')) {
$this->next_patient = Waitlist::where('email', $this->argument('email'))->first();
if (!$this->next_patient) {
$this->error("{$this->argument('email')} not found in the waitlist.");
return;
}
} else {
$this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first();
}
if ($this->next_patient) {
$this->register_user();
$this->remove_from_waitlist();

View File

@@ -0,0 +1,184 @@
<?php
namespace App\Console\Commands;
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\Notifications\Application\DeploymentFailed;
use App\Notifications\Application\DeploymentSuccess;
use App\Notifications\Application\StatusChanged;
use App\Notifications\Database\BackupFailed;
use App\Notifications\Database\BackupSuccess;
use App\Notifications\Test;
use App\Notifications\TransactionalEmails\InvitationLink;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage;
use Mail;
use Str;
use function Laravel\Prompts\select;
class TestEmail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'email:test';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send a test email to the admin';
/**
* Execute the console command.
*/
private ?MailMessage $mail = null;
public function handle()
{
$email = select(
'Which Email should be sent?',
options: [
'emails-test' => 'Test',
'application-deployment-success' => 'Application - Deployment Success',
'application-deployment-failed' => 'Application - Deployment Failed',
'application-status-changed' => 'Application - Status Changed',
'backup-success' => 'Database - Backup Success',
'backup-failed' => 'Database - Backup Failed',
'invitation-link' => 'Invitation Link',
'waitlist-invitation-link' => 'Waitlist Invitation Link',
'waitlist-confirmation' => 'Waitlist Confirmation',
],
);
$type = set_transanctional_email_settings();
if (!$type) {
throw new Exception('No email settings found.');
}
$this->mail = new MailMessage();
$this->mail->subject("Test Email");
switch ($email) {
case 'emails-test':
$this->mail = (new Test())->toMail();
break;
case 'application-deployment-success':
$application = Application::all()->first();
$this->mail = (new DeploymentSuccess($application, 'test'))->toMail();
$this->sendEmail();
break;
case 'application-deployment-failed':
$application = Application::all()->first();
$preview = ApplicationPreview::all()->first();
if (!$preview) {
$preview = ApplicationPreview::create([
'application_id' => $application->id,
'pull_request_id' => 1,
'pull_request_html_url' => 'http://example.com',
'fqdn' => $application->fqdn,
]);
}
$this->mail = (new DeploymentFailed($application, 'test'))->toMail();
$this->sendEmail();
$this->mail = (new DeploymentFailed($application, 'test', $preview))->toMail();
$this->sendEmail();
break;
case 'application-status-changed':
$application = Application::all()->first();
$this->mail = (new StatusChanged($application))->toMail();
$this->sendEmail();
break;
case 'backup-failed':
$backup = ScheduledDatabaseBackup::all()->first();
$db = StandalonePostgresql::all()->first();
if (!$backup) {
$backup = ScheduledDatabaseBackup::create([
'enabled' => true,
'frequency' => 'daily',
'save_s3' => false,
'database_id' => $db->id,
'database_type' => $db->getMorphClass(),
'team_id' => 0,
]);
}
$output = 'Because of an error, the backup of the database ' . $db->name . ' failed.';
$this->mail = (new BackupFailed($backup, $db, $output))->toMail();
$this->sendEmail();
break;
case 'backup-success':
$backup = ScheduledDatabaseBackup::all()->first();
$db = StandalonePostgresql::all()->first();
if (!$backup) {
$backup = ScheduledDatabaseBackup::create([
'enabled' => true,
'frequency' => 'daily',
'save_s3' => false,
'database_id' => $db->id,
'database_type' => $db->getMorphClass(),
'team_id' => 0,
]);
}
$this->mail = (new BackupSuccess($backup, $db))->toMail();
$this->sendEmail();
break;
case 'invitation-link':
$user = User::all()->first();
$invitation = TeamInvitation::whereEmail($user->email)->first();
if (!$invitation) {
$invitation = TeamInvitation::create([
'uuid' => Str::uuid(),
'email' => $user->email,
'team_id' => 1,
'link' => 'http://example.com',
]);
}
$this->mail = (new InvitationLink($user))->toMail();
$this->sendEmail();
break;
case 'waitlist-invitation-link':
$this->mail = new MailMessage();
$this->mail->view('emails.waitlist-invitation', [
'email' => 'test2@example.com',
'password' => "supersecretpassword",
]);
$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();
break;
}
}
private function sendEmail()
{
Mail::send(
[],
[],
fn (Message $message) => $message
->from(
'internal@example.com',
'Test Email',
)
->to('test@example.com')
->subject($this->mail->subject)
->html((string)$this->mail->render())
);
}
}

View File

@@ -18,22 +18,22 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void
{
// $schedule->call(fn() => $this->check_scheduled_backups($schedule))->everyTenSeconds();
if (is_dev()) {
if (isDev()) {
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new ResourceStatusJob)->everyMinute();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
$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);

View File

@@ -5,6 +5,7 @@ namespace App\Exceptions;
use App\Models\InstanceSettings;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Sentry\Laravel\Integration;
use Sentry\State\Scope;
use Throwable;
class Handler extends ExceptionHandler
@@ -45,9 +46,14 @@ class Handler extends ExceptionHandler
{
$this->reportable(function (Throwable $e) {
$this->settings = InstanceSettings::get();
if ($this->settings->do_not_track || is_dev()) {
if ($this->settings->do_not_track || isDev()) {
return;
}
app('sentry')->configureScope(
function (Scope $scope){
$scope->setUser(['id'=> config('sentry.server_name')]);
}
);
Integration::captureUnhandledException($e);
});
}

View File

@@ -12,7 +12,7 @@ class ApplicationController extends Controller
public function configuration()
{
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -29,7 +29,7 @@ class ApplicationController extends Controller
public function deployments()
{
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -49,7 +49,7 @@ class ApplicationController extends Controller
{
$deploymentUuid = request()->route('deployment_uuid');
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}

View File

@@ -5,11 +5,9 @@ namespace App\Http\Controllers;
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', [
return view('subscription.index', [
'settings' => InstanceSettings::get(),
]);
}
public function license()
{
if (!is_cloud()) {
if (!isCloud()) {
abort(404);
}
return view('settings.license', [
@@ -48,28 +40,17 @@ class Controller extends BaseController
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();
public function boarding() {
if (currentTeam()->boarding || isDev()) {
return view('boarding');
} else {
return redirect()->route('dashboard');
}
return view('dashboard', [
'servers' => $servers->count(),
'projects' => $projects->count(),
'resources' => $resources,
's3s' => $s3s,
]);
}
public function settings()
{
if (is_instance_admin()) {
if (isInstanceAdmin()) {
$settings = InstanceSettings::get();
$database = StandalonePostgresql::whereName('coolify-db')->first();
if ($database) {
@@ -89,9 +70,9 @@ class Controller extends BaseController
{
$invitations = [];
if (auth()->user()->isAdminFromSession()) {
$invitations = TeamInvitation::whereTeamId(auth()->user()->currentTeam()->id)->get();
$invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
}
return view('team.show', [
return view('team.index', [
'invitations' => $invitations,
]);
}
@@ -116,7 +97,7 @@ class Controller extends BaseController
{
$invitations = [];
if (auth()->user()->isAdminFromSession()) {
$invitations = TeamInvitation::whereTeamId(auth()->user()->currentTeam()->id)->get();
$invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
}
return view('team.members', [
'invitations' => $invitations,
@@ -140,7 +121,7 @@ class Controller extends BaseController
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);
@@ -162,7 +143,7 @@ class Controller extends BaseController
abort(401);
}
$invitation->delete();
return redirect()->route('team.show');
return redirect()->route('team.index');
} catch (Throwable $th) {
throw $th;
}

View File

@@ -11,7 +11,7 @@ class DatabaseController extends Controller
public function configuration()
{
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -29,7 +29,7 @@ class DatabaseController extends Controller
public function executions()
{
$backup_uuid = request()->route('backup_uuid');
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -50,13 +50,13 @@ class DatabaseController extends Controller
'database' => $database,
'backup' => $backup,
'executions' => $executions,
's3s' => auth()->user()->currentTeam()->s3s,
's3s' => currentTeam()->s3s,
]);
}
public function backups()
{
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -70,7 +70,7 @@ class DatabaseController extends Controller
}
return view('project.database.backups.all', [
'database' => $database,
's3s' => auth()->user()->currentTeam()->s3s,
's3s' => currentTeam()->s3s,
]);
}
}

View File

@@ -41,7 +41,7 @@ class MagicController extends Controller
{
$project = Project::firstOrCreate(
['name' => request()->query('name') ?? generate_random_name()],
['team_id' => auth()->user()->currentTeam()->id]
['team_id' => currentTeam()->id]
);
return response()->json([
'project_uuid' => $project->uuid
@@ -68,7 +68,7 @@ class MagicController extends Controller
],
);
auth()->user()->teams()->attach($team, ['role' => 'admin']);
session(['currentTeam' => $team]);
refreshSession();
return redirect(request()->header('Referer'));
}
}

View File

@@ -18,7 +18,7 @@ class ProjectController extends Controller
public function edit()
{
$projectUuid = request()->route('project_uuid');
$teamId = auth()->user()->currentTeam()->id;
$teamId = currentTeam()->id;
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();
if (!$project) {
return redirect()->route('dashboard');
@@ -29,7 +29,7 @@ class ProjectController extends Controller
public function show()
{
$projectUuid = request()->route('project_uuid');
$teamId = auth()->user()->currentTeam()->id;
$teamId = currentTeam()->id;
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first();
if (!$project) {
@@ -43,8 +43,9 @@ class ProjectController extends Controller
{
$type = request()->query('type');
$destination_uuid = request()->query('destination');
$server = requesT()->query('server');
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
@@ -59,6 +60,9 @@ class ProjectController extends Controller
'environment_name' => $environment->name,
'database_uuid' => $standalone_postgresql->uuid,
]);
}
if ($server) {
}
return view('project.new', [
'type' => $type
@@ -67,7 +71,7 @@ class ProjectController extends Controller
public function resources()
{
$project = auth()->user()->currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}

View File

@@ -12,20 +12,21 @@ class ServerController extends Controller
public function new_server()
{
if (!is_cloud() || is_instance_admin()) {
$privateKeys = PrivateKey::ownedByCurrentTeam()->get();
if (!isCloud()) {
return view('server.create', [
'limit_reached' => false,
'private_keys' => PrivateKey::ownedByCurrentTeam()->get(),
'private_keys' => $privateKeys,
]);
}
$servers = auth()->user()->currentTeam()->servers->count();
$subscription = auth()->user()->currentTeam()?->subscription->type();
$limits = config('constants.limits.server')[strtolower($subscription)];
$limit_reached = true ?? $servers >= $limits[$subscription];
$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,
]);
}
}

View File

@@ -38,7 +38,8 @@ 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,
],

View File

@@ -0,0 +1,256 @@
<?php
namespace App\Http\Livewire\Boarding;
use App\Actions\Server\InstallDocker;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use Illuminate\Support\Collection;
use Livewire\Component;
class Index extends Component
{
public string $currentState = 'welcome';
public ?Collection $privateKeys = null;
public ?int $selectedExistingPrivateKey = null;
public ?string $privateKeyType = null;
public ?string $privateKey = null;
public ?string $publicKey = null;
public ?string $privateKeyName = null;
public ?string $privateKeyDescription = null;
public ?PrivateKey $createdPrivateKey = null;
public ?Collection $servers = null;
public ?int $selectedExistingServer = null;
public ?string $remoteServerName = null;
public ?string $remoteServerDescription = null;
public ?string $remoteServerHost = null;
public ?int $remoteServerPort = 22;
public ?string $remoteServerUser = 'root';
public ?Server $createdServer = null;
public Collection|array $projects = [];
public ?int $selectedExistingProject = null;
public ?Project $createdProject = null;
public function mount()
{
$this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name();
if (isDev()) {
$this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
-----END OPENSSH PRIVATE KEY-----';
$this->privateKeyDescription = 'Created by Coolify';
$this->remoteServerDescription = 'Created by Coolify';
$this->remoteServerHost = 'coolify-testing-host';
}
}
public function welcome() {
if (isCloud()) {
return $this->setServerType('remote');
}
$this->currentState = 'select-server-type';
}
public function restartBoarding()
{
if ($this->createdServer) {
$this->createdServer->delete();
}
if ($this->createdPrivateKey) {
$this->createdPrivateKey->delete();
}
return redirect()->route('boarding');
}
public function skipBoarding()
{
currentTeam()->update([
'show_boarding' => false
]);
refreshSession();
return redirect()->route('dashboard');
}
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.');
}
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();
}
$this->currentState = 'create-private-key';
}
public function savePrivateKey()
{
$this->validate([
'privateKeyName' => 'required',
'privateKey' => 'required',
]);
$this->currentState = 'create-server';
}
public function saveServer()
{
$this->validate([
'remoteServerName' => 'required',
'remoteServerHost' => 'required',
'remoteServerPort' => 'required',
'remoteServerUser' => 'required',
]);
$this->privateKey = formatPrivateKey($this->privateKey);
$this->createdPrivateKey = PrivateKey::create([
'name' => $this->privateKeyName,
'description' => $this->privateKeyDescription,
'private_key' => $this->privateKey,
'team_id' => currentTeam()->id
]);
$this->createdServer = Server::create([
'name' => $this->remoteServerName,
'ip' => $this->remoteServerHost,
'port' => $this->remoteServerPort,
'user' => $this->remoteServerUser,
'description' => $this->remoteServerDescription,
'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) {
throw new \Exception('Server is not reachable.');
} else {
$this->createdServer->settings->update([
'is_reachable' => true,
]);
$this->emit('success', 'Server is reachable.');
}
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);
}
}
public function installDocker()
{
$activity = resolve(InstallDocker::class)($this->createdServer, currentTeam());
$this->emit('newMonitorActivity', $activity->id);
$this->currentState = 'select-proxy';
}
public function selectProxy(string|null $proxyType = null)
{
if (!$proxyType) {
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([
'name' => "My first project",
'team_id' => currentTeam()->id
]);
$this->currentState = 'create-resource';
}
public function showNewResource()
{
$this->skipBoarding();
return redirect()->route(
'project.resources.new',
[
'project_uuid' => $this->createdProject->uuid,
'environment_name' => 'production',
'server'=> $this->createdServer->id,
]
);
}
private function createNewPrivateKey()
{
$this->privateKeyName = generate_random_name();
$this->privateKeyDescription = 'Created by Coolify';
['private' => $this->privateKey, 'public' => $this->publicKey] = generateSSHKey();
}
public function render()
{
return view('livewire.boarding.index')->layout('layouts.boarding');
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Http\Livewire;
use App\Models\Project;
use App\Models\S3Storage;
use App\Models\Server;
use Livewire\Component;
class Dashboard extends Component
{
public int $projects = 0;
public int $servers = 0;
public int $s3s = 0;
public int $resources = 0;
public function mount()
{
$this->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');
}
}

View File

@@ -60,20 +60,19 @@ class StandaloneDocker extends Component
$found = $this->server->standaloneDockers()->where('network', $this->network)->first();
if ($found) {
$this->createNetworkAndAttachToProxy();
$this->addError('network', 'Network already added to this server.');
$this->emit('error', 'Network already added to this server.');
return;
} else {
$docker = ModelsStandaloneDocker::create([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->server_id,
'team_id' => auth()->user()->currentTeam()->id
]);
}
$this->createNetworkAndAttachToProxy();
return redirect()->route('destination.show', $docker->uuid);
} catch (\Exception $e) {
return general_error_handler(err: $e);
return general_error_handler(err: $e, that: $this);
}
}

View File

@@ -1,41 +0,0 @@
<?php
namespace App\Http\Livewire\Dev;
use App\Models\S3Storage;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
use Livewire\WithFileUploads;
class S3Test extends Component
{
use WithFileUploads;
public $s3;
public $file;
public function mount()
{
$this->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'));
}
}

View File

@@ -41,10 +41,9 @@ class DiscordSettings extends Component
public function saveModel()
{
ray($this->model);
$this->model->save();
if (is_a($this->model, Team::class)) {
session(['currentTeam' => $this->model]);
refreshSession();
}
$this->emit('success', 'Settings saved.');
}

View File

@@ -6,60 +6,148 @@ use App\Models\InstanceSettings;
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();
if ($settings->smtp_enabled) {
$team = auth()->user()->currentTeam();
$team = currentTeam();
$team->update([
'smtp_enabled' => $settings->smtp_enabled,
'smtp_from_address' => $settings->smtp_from_address,
@@ -72,55 +160,22 @@ class EmailSettings extends Component
'smtp_password' => $settings->smtp_password,
'smtp_timeout' => $settings->smtp_timeout,
]);
$this->decrypt();
if (is_a($team, Team::class)) {
session(['currentTeam' => $team]);
}
$this->model = $team;
refreshSession();
$this->team = $team;
$this->emit('success', 'Settings saved.');
} else {
$this->emit('error', 'Instance SMTP settings are not enabled.');
return;
}
}
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();
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;
}
}
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)) {
session(['currentTeam' => $this->model]);
}
$this->emit('success', 'Settings saved.');
$this->emit('error', 'Instance SMTP/Resend settings are not enabled.');
}
}

View File

@@ -26,7 +26,7 @@ class Change extends Component
try {
if ($this->private_key->isEmpty()) {
$this->private_key->delete();
auth()->user()->currentTeam()->privateKeys = PrivateKey::where('team_id', auth()->user()->currentTeam()->id)->get();
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
return redirect()->route('private-key.all');
}
$this->emit('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.');
@@ -38,10 +38,7 @@ class Change extends Component
public function changePrivateKey()
{
try {
$this->private_key->private_key = trim($this->private_key->private_key);
if (!str_ends_with($this->private_key->private_key, "\n")) {
$this->private_key->private_key .= "\n";
}
$this->private_key->private_key = formatPrivateKey($this->private_key->private_key);
$this->private_key->save();
refresh_server_connection($this->private_key);
} catch (\Exception $e) {

View File

@@ -32,7 +32,7 @@ class Create extends Component
'name' => $this->name,
'description' => $this->description,
'private_key' => $this->value,
'team_id' => auth()->user()->currentTeam()->id
'team_id' => currentTeam()->id
]);
if ($this->from === 'server') {
return redirect()->route('server.create');

View File

@@ -25,7 +25,7 @@ class AddEmpty extends Component
$project = Project::create([
'name' => $this->name,
'description' => $this->description,
'team_id' => auth()->user()->currentTeam()->id,
'team_id' => currentTeam()->id,
]);
return redirect()->route('project.show', $project->uuid);
} catch (\Exception $e) {

View File

@@ -136,15 +136,20 @@ class General extends Component
public function submit()
{
ray($this->application);
try {
$this->validate();
$domains = Str::of($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return Str::of($domain)->trim()->lower();
});
$port = get_port_from_dockerfile($this->application->dockerfile);
if ($port) {
$this->application->ports_exposes = $port;
if (data_get($this->application,'fqdn')) {
$domains = Str::of($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return Str::of($domain)->trim()->lower();
});
$this->application->fqdn = $domains->implode(',');
}
if ($this->application->dockerfile) {
$port = get_port_from_dockerfile($this->application->dockerfile);
if ($port) {
$this->application->ports_exposes = $port;
}
}
if ($this->application->base_directory && $this->application->base_directory !== '/') {
$this->application->base_directory = rtrim($this->application->base_directory, '/');
@@ -152,7 +157,6 @@ class General extends Component
if ($this->application->publish_directory && $this->application->publish_directory !== '/') {
$this->application->publish_directory = rtrim($this->application->publish_directory, '/');
}
$this->application->fqdn = data_get($domains->implode(','), '', null);
$this->application->save();
$this->emit('success', 'Application settings updated!');
} catch (\Exception $e) {

View File

@@ -2,7 +2,7 @@
namespace App\Http\Livewire\Project\Application;
use App\Jobs\ContainerStatusJob;
use App\Jobs\ApplicationContainerStatusJob;
use App\Models\Application;
use App\Notifications\Application\StatusChanged;
use Livewire\Component;
@@ -22,9 +22,8 @@ class Heading extends Component
public function check_status()
{
dispatch_sync(new ContainerStatusJob(
resource: $this->application,
container_name: generate_container_name($this->application->uuid),
dispatch_sync(new ApplicationContainerStatusJob(
application: $this->application,
));
$this->application->refresh();
}
@@ -58,12 +57,21 @@ class Heading extends Component
public function stop()
{
remote_process(
["docker rm -f {$this->application->uuid}"],
$this->application->destination->server
);
$this->application->status = 'stopped';
$this->application->save();
$this->application->environment->project->team->notify(new StatusChanged($this->application));
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
if ($containers->count() === 0) {
return;
}
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
remote_process(
["docker rm -f {$containerName}"],
$this->application->destination->server
);
$this->application->status = 'stopped';
$this->application->save();
// $this->application->environment->project->team->notify(new StatusChanged($this->application));
}
}
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Http\Livewire\Project\Application;
use App\Jobs\ContainerStatusJob;
use App\Jobs\ApplicationContainerStatusJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use Illuminate\Support\Collection;
@@ -25,10 +25,9 @@ class Previews extends Component
public function loadStatus($pull_request_id)
{
dispatch(new ContainerStatusJob(
resource: $this->application,
container_name: generate_container_name($this->application->uuid, $pull_request_id),
pull_request_id: $pull_request_id
dispatch(new ApplicationContainerStatusJob(
application: $this->application,
pullRequestId: $pull_request_id
));
}
@@ -82,7 +81,7 @@ class Previews extends Component
public function stop(int $pull_request_id)
{
try {
$container_name = generate_container_name($this->application->uuid, $pull_request_id);
$container_name = generateApplicationContainerName($this->application->uuid, $pull_request_id);
ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $this->application->destination->server, throwError: false);

View File

@@ -29,7 +29,7 @@ class Source extends Component
private function get_private_keys()
{
$this->private_keys = PrivateKey::whereTeamId(auth()->user()->currentTeam()->id)->get()->reject(function ($key) {
$this->private_keys = PrivateKey::whereTeamId(currentTeam()->id)->get()->reject(function ($key) {
return $key->id == $this->application->private_key_id;
});
}

View File

@@ -39,7 +39,7 @@ class CreateScheduledBackup extends Component
's3_storage_id' => $this->s3_storage_id,
'database_id' => $this->database->id,
'database_type' => $this->database->getMorphClass(),
'team_id' => auth()->user()->currentTeam()->id,
'team_id' => currentTeam()->id,
]);
$this->emit('refreshScheduledBackups');
} catch (\Exception $e) {

View File

@@ -3,7 +3,7 @@
namespace App\Http\Livewire\Project\Database;
use App\Actions\Database\StartPostgresql;
use App\Jobs\ContainerStatusJob;
use App\Jobs\DatabaseContainerStatusJob;
use App\Notifications\Application\StatusChanged;
use Livewire\Component;
@@ -25,9 +25,8 @@ class Heading extends Component
public function check_status()
{
dispatch_sync(new ContainerStatusJob(
resource: $this->database,
container_name: generate_container_name($this->database->uuid),
dispatch_sync(new DatabaseContainerStatusJob(
database: $this->database,
));
$this->database->refresh();
}
@@ -45,7 +44,7 @@ class Heading extends Component
);
$this->database->status = 'stopped';
$this->database->save();
$this->database->environment->project->team->notify(new StatusChanged($this->database));
// $this->database->environment->project->team->notify(new StatusChanged($this->database));
}
public function start()

View File

@@ -11,7 +11,7 @@ class EmptyProject extends Component
{
$project = Project::create([
'name' => generate_random_name(),
'team_id' => auth()->user()->currentTeam()->id,
'team_id' => currentTeam()->id,
]);
return redirect()->route('project.show', ['project_uuid' => $project->uuid, 'environment_name' => 'production']);
}

View File

@@ -7,15 +7,19 @@ use App\Models\GithubApp;
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();

View File

@@ -43,19 +43,19 @@ class GithubPrivateRepositoryDeployKey extends Component
'publish_directory' => 'Publish directory',
];
private object $repository_url_parsed;
private GithubApp|GitlabApp $git_source;
private GithubApp|GitlabApp|null $git_source = null;
private string $git_host;
private string $git_repository;
private string $git_branch;
public function mount()
{
if (is_dev()) {
if (isDev()) {
$this->repository_url = 'https://github.com/coollabsio/coolify-examples';
}
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->private_keys = PrivateKey::where('team_id', auth()->user()->currentTeam()->id)->where('id', '!=', 0)->get();
$this->private_keys = PrivateKey::where('team_id', currentTeam()->id)->where('id', '!=', 0)->get();
}
public function instantSave()

View File

@@ -39,13 +39,13 @@ class PublicGitRepository extends Component
'publish_directory' => 'publish directory',
];
private object $repository_url_parsed;
private GithubApp|GitlabApp $git_source;
private GithubApp|GitlabApp|null $git_source = null;
private string $git_host;
private string $git_repository;
public function mount()
{
if (is_dev()) {
if (isDev()) {
$this->repository_url = 'https://github.com/coollabsio/coolify-examples';
$this->port = 3000;
}
@@ -67,18 +67,17 @@ class PublicGitRepository extends Component
public function load_branch()
{
$this->branch_found = false;
try {
$this->branch_found = false;
$this->validate([
'repository_url' => 'required|url'
]);
$this->get_git_source();
try {
$this->get_branch();
$this->selected_branch = $this->git_branch;
$this->get_branch();
$this->selected_branch = $this->git_branch;
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
if (!$this->branch_found && $this->git_branch == 'main') {
try {
$this->git_branch = 'master';
@@ -103,6 +102,9 @@ class PublicGitRepository extends Component
} elseif ($this->git_host == 'bitbucket.org') {
// Not supported yet
}
if (is_null($this->git_source)) {
throw new \Exception('Git source not found. What?!');
}
}
private function get_branch()

View File

@@ -3,18 +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 $servers = [];
public $destinations = [];
public Countable|array|Server $servers;
public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = [];
public array $parameters;
protected $queryString = [
'server',
];
public function mount()
{
$this->parameters = get_route_parameters();
@@ -23,13 +33,27 @@ class Select extends Component
public function set_type(string $type)
{
$this->type = $type;
if (count($this->servers) === 1) {
$server = $this->servers->first();
$this->set_server($server);
if (count($server->destinations()) === 1) {
$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';
}
@@ -46,6 +70,6 @@ class Select extends Component
public function load_servers()
{
$this->servers = Server::ownedByCurrentTeam()->get();
$this->servers = Server::isUsable()->get();
}
}

View File

@@ -19,7 +19,7 @@ class SimpleDockerfile extends Component
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
if (is_dev()) {
if (isDev()) {
$this->dockerfile = 'FROM nginx
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
@@ -59,6 +59,10 @@ CMD ["nginx", "-g", "daemon off;"]
'source_id' => 0,
'source_type' => GithubApp::class
]);
$application->update([
'name' => 'dockerfile-' . $application->id
]);
redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Livewire\Server;
use App\Models\Server;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Component;
class All extends Component
{
public ?Collection $servers = null;
public function mount () {
$this->servers = Server::ownedByCurrentTeam()->get();
}
public function render()
{
return view('livewire.server.all');
}
}

View File

@@ -4,10 +4,12 @@ namespace App\Http\Livewire\Server;
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;
@@ -42,7 +44,7 @@ class Form extends Component
public function installDocker()
{
$activity = resolve(InstallDocker::class)($this->server, auth()->user()->currentTeam());
$activity = resolve(InstallDocker::class)($this->server, currentTeam());
$this->emit('newMonitorActivity', $activity->id);
}
@@ -64,14 +66,20 @@ class Form extends Component
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();

View File

@@ -65,7 +65,7 @@ class ByIp extends Component
'ip' => $this->ip,
'user' => $this->user,
'port' => $this->port,
'team_id' => auth()->user()->currentTeam()->id,
'team_id' => currentTeam()->id,
'private_key_id' => $this->private_key_id,
]);
$server->settings->is_part_of_swarm = $this->is_part_of_swarm;

View File

@@ -35,7 +35,7 @@ class Proxy extends Component
$this->emit('proxyStatusUpdated');
}
public function select_proxy(string $proxy_type)
public function select_proxy(ProxyTypes $proxy_type)
{
$this->server->proxy->type = $proxy_type;
$this->server->proxy->status = 'exited';

View File

@@ -17,7 +17,7 @@ class Deploy extends Component
$this->server->proxy->last_applied_settings &&
$this->server->proxy->last_saved_settings !== $this->server->proxy->last_applied_settings
) {
$this->emit('saveConfiguration', $server);
$this->emit('saveConfiguration', $this->server);
}
$activity = resolve(StartProxy::class)($this->server);
$this->emit('newMonitorActivity', $activity->id);

View File

@@ -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');
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Livewire\Server;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Show extends Component
{
use AuthorizesRequests;
public ?Server $server = null;
public function mount()
{
try {
$this->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');
}
}

View File

@@ -6,7 +6,7 @@ use App\Models\Server;
use Livewire\Component;
use Masmerise\Toaster\Toaster;
class PrivateKey extends Component
class ShowPrivateKey extends Component
{
public Server $server;
public $privateKeys;

View File

@@ -65,7 +65,7 @@ class Backup extends Component
'frequency' => '0 0 * * *',
'database_id' => $this->database->id,
'database_type' => 'App\Models\StandalonePostgresql',
'team_id' => auth()->user()->currentTeam()->id,
'team_id' => currentTeam()->id,
]);
$this->database->refresh();
$this->backup->refresh();

View File

@@ -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()

View File

@@ -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()

View File

@@ -40,8 +40,11 @@ class Create extends Component
'custom_user' => $this->custom_user,
'custom_port' => $this->custom_port,
'is_system_wide' => $this->is_system_wide,
'team_id' => auth()->user()->currentTeam()->id,
'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);

View File

@@ -10,14 +10,14 @@ class Actions extends Component
public function cancel()
{
try {
$subscription_id = auth()->user()->currentTeam()->subscription->lemon_subscription_id;
$subscription_id = currentTeam()->subscription->lemon_subscription_id;
if (!$subscription_id) {
throw new \Exception('No subscription found');
}
$response = Http::withHeaders([
'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json',
'Authorization' => 'Bearer ' . config('coolify.lemon_squeezy_api_key'),
'Authorization' => 'Bearer ' . config('subscription.lemon_squeezy_api_key'),
])->delete('https://api.lemonsqueezy.com/v1/subscriptions/' . $subscription_id);
$json = $response->json();
if ($response->failed()) {
@@ -37,14 +37,14 @@ class Actions extends Component
public function resume()
{
try {
$subscription_id = auth()->user()->currentTeam()->subscription->lemon_subscription_id;
$subscription_id = currentTeam()->subscription->lemon_subscription_id;
if (!$subscription_id) {
throw new \Exception('No subscription found');
}
$response = Http::withHeaders([
'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json',
'Authorization' => 'Bearer ' . config('coolify.lemon_squeezy_api_key'),
'Authorization' => 'Bearer ' . config('subscription.lemon_squeezy_api_key'),
])->patch('https://api.lemonsqueezy.com/v1/subscriptions/' . $subscription_id, [
'data' => [
'type' => 'subscriptions',
@@ -69,4 +69,8 @@ class Actions extends Component
return general_error_handler($e, $this);
}
}
public function stripeCustomerPortal() {
$session = getStripeCustomerPortalSession(currentTeam());
redirect($session->url);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Http\Livewire\Subscription;
use Livewire\Component;
use Stripe\Stripe;
use Stripe\Checkout\Session;
class PricingPlans extends Component
{
public function subscribeStripe($type)
{
Stripe::setApiKey(config('subscription.stripe_api_key'));
switch ($type) {
case 'basic-monthly':
$priceId = config('subscription.stripe_price_id_basic_monthly');
break;
case 'basic-yearly':
$priceId = config('subscription.stripe_price_id_basic_yearly');
break;
case 'ultimate-monthly':
$priceId = config('subscription.stripe_price_id_ultimate_monthly');
break;
case 'pro-monthly':
$priceId = config('subscription.stripe_price_id_pro_monthly');
break;
case 'pro-yearly':
$priceId = config('subscription.stripe_price_id_pro_yearly');
break;
case 'ultimate-yearly':
$priceId = config('subscription.stripe_price_id_ultimate_yearly');
break;
default:
$priceId = config('subscription.stripe_price_id_basic_monthly');
break;
}
if (!$priceId) {
$this->emit('error', 'Price ID not found! Please contact the administrator.');
return;
}
$payload = [
'client_reference_id' => auth()->user()->id . ':' . currentTeam()->id,
'line_items' => [[
'price' => $priceId,
'quantity' => 1,
]],
'tax_id_collection' => [
'enabled' => true,
],
'mode' => 'subscription',
'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.index', ['cancelled' => true]),
];
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
if ($customer) {
$payload['customer'] = $customer;
$payload['customer_update'] = [
'name' => 'auto'
];
} else {
$payload['customer_email'] = auth()->user()->email;
}
$session = Session::create($payload);
return redirect($session->url, 303);
}
}

View File

@@ -29,8 +29,8 @@ class Create extends Component
'personal_team' => false,
]);
auth()->user()->teams()->attach($team, ['role' => 'admin']);
session(['currentTeam' => $team]);
return redirect()->route('team.show');
refreshSession();
return redirect()->route('team.index');
} catch (\Throwable $th) {
return general_error_handler($th, $this);
}

View File

@@ -9,7 +9,7 @@ class Delete extends Component
{
public function delete()
{
$currentTeam = auth()->user()->currentTeam();
$currentTeam = currentTeam();
$currentTeam->delete();
$team = auth()->user()->teams()->first();
@@ -24,7 +24,7 @@ class Delete extends Component
}
});
session(['currentTeam' => $team]);
return redirect()->route('team.show');
refreshSession();
return redirect()->route('team.index');
}
}

View File

@@ -19,7 +19,7 @@ class Form extends Component
public function mount()
{
$this->team = auth()->user()->currentTeam();
$this->team = currentTeam();
}
public function submit()
@@ -27,8 +27,7 @@ class Form extends Component
$this->validate();
try {
$this->team->save();
session(['currentTeam' => $this->team]);
$this->emit('reloadWindow');
refreshSession();
} catch (\Throwable $th) {
return general_error_handler($th, $this);
}

View File

@@ -14,10 +14,11 @@ class Invitations extends Component
{
TeamInvitation::find($invitation_id)->delete();
$this->refreshInvitations();
$this->emit('success', 'Invitation revoked.');
}
public function refreshInvitations()
{
$this->invitations = TeamInvitation::whereTeamId(auth()->user()->currentTeam()->id)->get();
$this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
}
}

View File

@@ -15,7 +15,7 @@ class InviteLink extends Component
public function mount()
{
$this->email = is_dev() ? 'test3@example.com' : '';
$this->email = isDev() ? 'test3@example.com' : '';
}
public function viaEmail()
@@ -35,9 +35,9 @@ class InviteLink extends Component
return general_error_handler(that: $this, customErrorMessage: "$this->email must be registered first (or activate transactional emails to invite via email).");
}
$member_emails = auth()->user()->currentTeam()->members()->get()->pluck('email');
$member_emails = currentTeam()->members()->get()->pluck('email');
if ($member_emails->contains($this->email)) {
return general_error_handler(that: $this, customErrorMessage: "$this->email is already a member of " . auth()->user()->currentTeam()->name . ".");
return general_error_handler(that: $this, customErrorMessage: "$this->email is already a member of " . currentTeam()->name . ".");
}
$invitation = TeamInvitation::whereEmail($this->email);
@@ -53,7 +53,7 @@ class InviteLink extends Component
}
TeamInvitation::firstOrCreate([
'team_id' => auth()->user()->currentTeam()->id,
'team_id' => currentTeam()->id,
'uuid' => $uuid,
'email' => $this->email,
'role' => $this->role,

View File

@@ -11,19 +11,19 @@ class Member extends Component
public function makeAdmin()
{
$this->member->teams()->updateExistingPivot(auth()->user()->currentTeam()->id, ['role' => 'admin']);
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'admin']);
$this->emit('reloadWindow');
}
public function makeReadonly()
{
$this->member->teams()->updateExistingPivot(auth()->user()->currentTeam()->id, ['role' => 'member']);
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']);
$this->emit('reloadWindow');
}
public function remove()
{
$this->member->teams()->detach(auth()->user()->currentTeam());
$this->member->teams()->detach(currentTeam());
$this->emit('reloadWindow');
}
}

View File

@@ -36,7 +36,7 @@ class Create extends Component
public function mount()
{
if (is_dev()) {
if (isDev()) {
$this->name = 'Local MinIO';
$this->description = 'Local MinIO';
$this->key = 'minioadmin';
@@ -62,7 +62,7 @@ class Create extends Component
} else {
$this->storage->endpoint = $this->endpoint;
}
$this->storage->team_id = auth()->user()->currentTeam()->id;
$this->storage->team_id = currentTeam()->id;
$this->storage->testConnection();
$this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
$this->storage->save();

View File

@@ -18,7 +18,7 @@ class Upgrade extends Component
$this->latestVersion = get_latest_version_of_coolify();
$currentVersion = config('version');
version_compare($currentVersion, $this->latestVersion, '<') ? $this->isUpgradeAvailable = true : $this->isUpgradeAvailable = false;
if (is_dev()) {
if (isDev()) {
$this->isUpgradeAvailable = true;
}
$settings = InstanceSettings::get();

View File

@@ -1,23 +1,28 @@
<?php
namespace App\Http\Livewire;
namespace App\Http\Livewire\Waitlist;
use App\Jobs\SendConfirmationForWaitlistJob;
use App\Models\User;
use App\Models\Waitlist as ModelsWaitlist;
use App\Models\Waitlist;
use Livewire\Component;
class Waitlist extends Component
class Index extends Component
{
public string $email;
public int $waiting_in_line = 0;
public int $waitingInLine = 0;
protected $rules = [
'email' => 'required|email',
];
public function render()
{
return view('livewire.waitlist.index')->layout('layouts.simple');
}
public function mount()
{
if (is_dev()) {
$this->waitingInLine = Waitlist::whereVerified(true)->count();
if (isDev()) {
$this->email = 'waitlist@example.com';
}
}
@@ -29,7 +34,7 @@ class Waitlist extends Component
if ($already_registered) {
throw new \Exception('You are already on the waitlist or registered. <br>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. <br>Please check your email to verify your email address.');
@@ -38,7 +43,7 @@ class Waitlist extends Component
$this->emit('error', 'You are already on the waitlist. <br>You will be notified when your turn comes. <br>Thank you.');
return;
}
$waitlist = ModelsWaitlist::create([
$waitlist = Waitlist::create([
'email' => $this->email,
'type' => 'registration',
]);

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IsBoardingFlow
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
ray()->showQueries()->color('orange');
if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect('boarding');
}
return $next($request);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IsSubscriptionValid
{
public function handle(Request $request, Closure $next): Response
{
if (isInstanceAdmin()) {
return $next($request);
}
if (!auth()->user() || !isCloud()) {
if ($request->path() === 'subscription') {
return redirect('/');
} else {
return $next($request);
}
}
if (isSubscriptionActive() && $request->path() === 'subscription') {
// ray('active subscription Middleware');
return redirect('/');
}
if (isSubscriptionOnGracePeriod()) {
// ray('is_subscription_in_grace_period Middleware');
return $next($request);
}
if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) {
// ray('SubscriptionValid Middleware');
if (!in_array($request->path(), allowedPathsForUnsubscribedAccounts())) {
return redirect('subscription');
} else {
return $next($request);
}
}
return $next($request);
}
}

View File

@@ -1,53 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SubscriptionValid
{
public function handle(Request $request, Closure $next): Response
{
if (!auth()->user() || !is_cloud()) {
if ($request->path() === 'subscription') {
return redirect('/');
} else {
return $next($request);
}
}
$is_instance_admin = is_instance_admin();
if ($is_instance_admin) {
return $next($request);
}
if (is_subscription_active() && $request->path() === 'subscription') {
return redirect('/');
}
if (is_subscription_in_grace_period()) {
return $next($request);
}
if (!is_subscription_active() && !is_subscription_in_grace_period()) {
ray('SubscriptionValid Middleware');
$allowed_paths = [
'subscription',
'login',
'register',
'waitlist',
'force-password-reset',
'logout',
'livewire/message/force-password-reset',
'livewire/message/check-license',
'livewire/message/switch-team',
];
if (!in_array($request->path(), $allowed_paths)) {
return redirect('subscription');
} else {
return $next($request);
}
}
return $next($request);
}
}

View File

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

View File

@@ -20,6 +20,7 @@ use Illuminate\Bus\Queueable;
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;
@@ -50,6 +51,7 @@ class ApplicationDeploymentJob implements ShouldQueue
private ApplicationPreview|null $preview = null;
private string $container_name;
private string|null $currently_running_container_name = null;
private string $workdir;
private string $configuration_dir;
private string $build_workdir;
@@ -64,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();
@@ -86,7 +94,7 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->build_workdir = "{$this->workdir}" . rtrim($this->application->base_directory, '/');
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->container_name = generate_container_name($this->application->uuid, $this->pull_request_id);
$this->container_name = generateApplicationContainerName($this->application->uuid, $this->pull_request_id);
$this->private_key_location = save_private_key_for_server($this->server);
$this->saved_outputs = collect();
@@ -113,6 +121,10 @@ class ApplicationDeploymentJob implements ShouldQueue
public function handle(): void
{
// ray()->measure();
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id);
if ($containers->count() > 0) {
$this->currently_running_container_name = data_get($containers[0], 'Names');
}
$this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
@@ -131,6 +143,7 @@ class ApplicationDeploymentJob implements ShouldQueue
} catch (Exception $e) {
ray($e);
$this->fail($e);
throw $e;
} finally {
if (isset($this->docker_compose_base64)) {
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
@@ -175,9 +188,9 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->stop_running_container();
$this->start_by_compose_file();
$this->rolling_update();
}
private function deploy()
{
$this->execute_remote_command(
@@ -206,8 +219,7 @@ class ApplicationDeploymentJob implements ShouldQueue
"echo 'Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped...'"
]);
$this->generate_compose_file();
$this->stop_running_container();
$this->start_by_compose_file();
$this->rolling_update();
return;
}
}
@@ -219,8 +231,54 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->stop_running_container();
$this->rolling_update();
}
private function rolling_update()
{
$this->start_by_compose_file();
$this->health_check();
$this->stop_running_container();
}
private function health_check()
{
ray('New container name: ',$this->container_name);
if ($this->container_name) {
$counter = 0;
$this->execute_remote_command(
[
"echo 'Waiting for health check to pass on the new version of your application.'"
],
);
while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command(
[
"echo 'Attempt {$counter} of {$this->application->health_check_retries}'"
],
[
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
"hidden" => true,
"save" => "health_check"
],
);
$this->execute_remote_command(
[
"echo 'New application version health check status: {$this->saved_outputs->get('health_check')}'"
],
);
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->execute_remote_command(
[
"echo 'Rolling update completed.'"
],
);
break;
}
$counter++;
sleep($this->application->health_check_interval);
}
}
}
private function deploy_pull_request()
{
@@ -241,8 +299,7 @@ class ApplicationDeploymentJob implements ShouldQueue
// $this->generate_build_env_variables();
// $this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->stop_running_container();
$this->start_by_compose_file();
$this->rolling_update();
}
private function prepare_builder_image()
@@ -409,7 +466,7 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->container_name => [
'image' => $this->production_image_name,
'container_name' => $this->container_name,
'restart' => 'always',
'restart' => RESTART_MODE,
'environment' => $environment_variables,
'labels' => $this->set_labels_for_applications(),
'expose' => $ports,
@@ -539,8 +596,8 @@ class ApplicationDeploymentJob implements ShouldQueue
$schema = $url->getScheme();
$slug = Str::slug($host . $path);
$http_label = "{$this->application->uuid}-{$slug}-http";
$https_label = "{$this->application->uuid}-{$slug}-https";
$http_label = "{$this->container_name}-{$slug}-http";
$https_label = "{$this->container_name}-{$slug}-https";
if ($schema === 'https') {
// Set labels for https
@@ -647,23 +704,22 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function stop_running_container()
{
$this->execute_remote_command(
["echo -n 'Removing old running application.'"],
[$this->execute_in_builder("docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true],
);
if ($this->currently_running_container_name) {
$this->execute_remote_command(
["echo -n 'Removing old application version.'"],
[$this->execute_in_builder("docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true],
);
}
}
private function start_by_compose_file()
{
$this->execute_remote_command(
["echo -n 'Starting new application... '"],
["echo -n 'Rolling update started.'"],
[$this->execute_in_builder("docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true],
["echo 'Done. 🎉'"],
);
}
private function generate_build_env_variables()
{
$this->build_args = collect(["--build-arg SOURCE_COMMIT={$this->commit}"]);

View File

@@ -22,7 +22,9 @@ class CheckResaleLicenseJob implements ShouldQueue
try {
resolve(CheckResaleLicense::class)();
} catch (\Throwable $th) {
send_internal_notification('CheckResaleLicenseJob failed with: ' . $th->getMessage());
ray($th);
throw $th;
}
}
}

View File

@@ -31,12 +31,13 @@ class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique
} catch (\Exception $e) {
send_internal_notification('CleanupInstanceStuffsJob failed with error: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
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();
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\ApplicationPreview;
use App\Notifications\Application\StatusChanged;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ContainerStatusJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public string $container_name;
public string|null $pull_request_id;
public $resource;
public function __construct($resource, string $container_name, string|null $pull_request_id = null)
{
$this->resource = $resource;
$this->container_name = $container_name;
$this->pull_request_id = $pull_request_id;
}
public function uniqueId(): string
{
return $this->container_name;
}
public function handle(): void
{
try {
$status = get_container_status(server: $this->resource->destination->server, container_id: $this->container_name, throwError: false);
if ($this->resource->status === 'running' && $status !== 'running') {
$this->resource->environment->project->team->notify(new StatusChanged($this->resource));
}
if ($this->pull_request_id) {
$preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->resource->id, $this->pull_request_id);
$preview->status = $status;
$preview->save();
} else {
$this->resource->status = $status;
$this->resource->save();
}
} catch (\Exception $e) {
ray($e->getMessage());
}
}
}

View File

@@ -28,7 +28,6 @@ class CoolifyTask implements ShouldQueue
*/
public function handle(): void
{
$remote_process = resolve(RunRemoteProcess::class, [
'activity' => $this->activity,
'ignore_errors' => $this->ignore_errors,

View File

@@ -64,35 +64,42 @@ class DatabaseBackupJob implements ShouldQueue
public function handle(): void
{
if ($this->database_status !== 'running') {
ray('database not running');
return;
}
$this->container_name = $this->database->uuid;
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name;
try {
if ($this->database_status !== 'running') {
ray('database not running');
return;
}
$this->container_name = $this->database->uuid;
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name;
if ($this->database->name === 'coolify-db') {
$this->container_name = "coolify-db";
$ip = Str::slug($this->server->ip);
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
}
$this->backup_file = "/dumpall-" . Carbon::now()->timestamp . ".sql";
$this->backup_location = $this->backup_dir . $this->backup_file;
if ($this->database->name === 'coolify-db') {
$this->container_name = "coolify-db";
$ip = Str::slug($this->server->ip);
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
}
$this->backup_file = "/dumpall-" . Carbon::now()->timestamp . ".sql";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
if ($this->database_type === 'standalone-postgresql') {
$this->backup_standalone_postgresql();
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
if ($this->database_type === 'standalone-postgresql') {
$this->backup_standalone_postgresql();
}
$this->calculate_size();
$this->remove_old_backups();
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->save_backup_logs();
// TODO: Notify user
} catch (\Throwable $th) {
ray($th->getMessage());
send_internal_notification('DatabaseBackupJob failed with: ' . $th->getMessage());
throw $th;
}
$this->calculate_size();
$this->remove_old_backups();
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->save_backup_logs();
// TODO: Notify user
}
private function backup_standalone_postgresql(): void

View File

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

View File

@@ -7,6 +7,7 @@ use Illuminate\Bus\Queueable;
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,48 +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 (is_dev()) {
$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) {
ray($e->getMessage());
send_internal_notification('DockerCleanupJob failed with: ' . $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();
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);
}
}

View File

@@ -22,18 +22,25 @@ class ProxyCheckJob implements ShouldQueue
{
try {
$container_name = 'coolify-proxy';
$servers = Server::isUsable()->whereNotNull('proxy')->get();
$servers = Server::all();
foreach ($servers as $server) {
$status = get_container_status(server: $server, container_id: $container_name);
if (
$server->settings->is_reachable === false || $server->settings->is_usable === false
) {
continue;
}
$status = getContainerStatus(server: $server, container_id: $container_name);
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());
//throw $th;
send_internal_notification('ProxyCheckJob failed with: ' . $th->getMessage());
throw $th;
}
}
}

View File

@@ -39,9 +39,9 @@ class ProxyContainerStatusJob implements ShouldQueue, ShouldBeUnique
public function handle(): void
{
try {
$container = get_container_status(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: true);
$status = $container['State']['Status'];
if ($this->server->proxy->status !== $status) {
$container = getContainerStatus(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: false);
$status = data_get($container, 'State.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'];
@@ -57,6 +57,9 @@ class ProxyContainerStatusJob implements ShouldQueue, ShouldBeUnique
$this->server->proxy->status = 'exited';
$this->server->save();
}
send_internal_notification('ProxyContainerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -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;
@@ -23,14 +25,20 @@ class ProxyStartJob implements ShouldQueue
try {
$container_name = 'coolify-proxy';
ray('Starting proxy for server: ' . $this->server->name);
$status = get_container_status(server: $this->server, container_id: $container_name);
$status = getContainerStatus(server: $this->server, container_id: $container_name);
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());
ray($th->getMessage());
//throw $th;
throw $th;
}
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Jobs;
use App\Models\Application;
use App\Models\StandalonePostgresql;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -15,23 +16,31 @@ class ResourceStatusJob implements ShouldQueue, ShouldBeUnique
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $applications;
public $postgresqls;
public function __construct()
{
$this->applications = Application::all();
$this->postgresqls = StandalonePostgresql::all();
}
public function handle(): void
{
try {
foreach ($this->applications as $application) {
dispatch(new ContainerStatusJob(
resource: $application,
container_name: generate_container_name($application->uuid),
dispatch(new ApplicationContainerStatusJob(
application: $application,
));
}
} catch (\Exception $e) {
ray($e->getMessage());
foreach ($this->postgresqls as $postgresql) {
dispatch(new DatabaseContainerStatusJob(
database: $postgresql,
));
}
} catch (\Exception $th) {
send_internal_notification('ResourceStatusJob failed with: ' . $th->getMessage());
ray($th);
throw $th;
}
}
}

View File

@@ -37,7 +37,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 with error: ' . $th->getMessage());
send_internal_notification("SendConfirmationForWaitlistJob failed for {$mail} with error: " . $th->getMessage());
ray($th->getMessage());
throw $th;
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Jobs;
use App\Models\Team;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SubscriptionInvoiceFailedJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(protected Team $team)
{
}
public function handle()
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage();
$mail->view('emails.subscription-invoice-failed', [
'stripeCustomerPortal' => $session->url,
]);
$mail->subject('Your last payment was failed for Coolify Cloud.');
$this->team->members()->each(function ($member) use ($mail) {
ray($member);
if ($member->isAdmin()) {
send_user_an_email($mail, $member->email);
}
});
} catch (\Throwable $th) {
send_internal_notification('SubscriptionInvoiceFailedJob failed with: ' . $th->getMessage());
ray($th->getMessage());
throw $th;
}
}
}

View File

@@ -4,15 +4,7 @@ namespace App\Models;
class ApplicationPreview extends BaseModel
{
protected $fillable = [
'uuid',
'pull_request_id',
'pull_request_html_url',
'pull_request_issue_comment_id',
'fqdn',
'status',
'application_id',
];
protected $guarded = [];
static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id)
{

View File

@@ -41,7 +41,7 @@ class EnvironmentVariable extends Model
private function get_environment_variables(string $environment_variable): string|null
{
// $team_id = auth()->user()->currentTeam()->id;
// $team_id = currentTeam()->id;
if (str_contains(trim($environment_variable), '{{') && str_contains(trim($environment_variable), '}}')) {
$environment_variable = preg_replace('/\s+/', '', $environment_variable);
$environment_variable = str_replace('{{', '', $environment_variable);

View File

@@ -19,12 +19,12 @@ class GithubApp extends BaseModel
static public function public()
{
return GithubApp::whereTeamId(auth()->user()->currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
}
static public function private()
{
return GithubApp::whereTeamId(auth()->user()->currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get();
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get();
}
protected static function booted(): void

View File

@@ -13,6 +13,7 @@ class InstanceSettings extends Model implements SendsEmail
protected $guarded = [];
protected $casts = [
'resale_license' => 'encrypted',
'smtp_password' => 'encrypted',
];
public static function get()

View File

@@ -16,7 +16,7 @@ class PrivateKey extends BaseModel
static public function ownedByCurrentTeam(array $select = ['*'])
{
$selectArray = collect($select)->concat(['id']);
return PrivateKey::whereTeamId(auth()->user()->currentTeam()->id)->select($selectArray->all());
return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all());
}
public function isEmpty()

View File

@@ -4,16 +4,11 @@ namespace App\Models;
class Project extends BaseModel
{
protected $fillable = [
'name',
'description',
'team_id',
'project_id'
];
protected $guarded = [];
static public function ownedByCurrentTeam()
{
return Project::whereTeamId(auth()->user()->currentTeam()->id)->orderBy('name');
return Project::whereTeamId(currentTeam()->id)->orderBy('name');
}
protected static function booted()

View File

@@ -17,7 +17,7 @@ class S3Storage extends BaseModel
static public function ownedByCurrentTeam(array $select = ['*'])
{
$selectArray = collect($select)->concat(['id']);
return S3Storage::whereTeamId(auth()->user()->currentTeam()->id)->select($selectArray->all())->orderBy('name');
return S3Storage::whereTeamId(currentTeam()->id)->select($selectArray->all())->orderBy('name');
}
public function awsUrl()

View File

@@ -10,21 +10,43 @@ class Server extends BaseModel
{
use SchemalessAttributesTrait;
protected static function booted()
{
static::created(function ($server) {
ServerSetting::create([
'server_id' => $server->id,
]);
if ($server->id === 0) {
StandaloneDocker::create([
'id' => 0,
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
]);
} else {
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
]);
}
});
static::deleting(function ($server) {
$server->destinations()->each(function ($destination) {
$destination->delete();
});
$server->settings()->delete();
});
}
public $casts = [
'proxy' => SchemalessAttributes::class,
];
protected $schemalessAttributes = [
'proxy',
];
protected $fillable = [
'name',
'ip',
'user',
'port',
'team_id',
'private_key_id',
'proxy',
];
protected $guarded = [];
static public function isReachable()
{
@@ -33,8 +55,9 @@ class Server extends BaseModel
static public function ownedByCurrentTeam(array $select = ['*'])
{
$teamId = currentTeam()->id;
$selectArray = collect($select)->concat(['id']);
return Server::whereTeamId(auth()->user()->currentTeam()->id)->with('settings')->select($selectArray->all())->orderBy('name');
return Server::whereTeamId($teamId)->with('settings')->select($selectArray->all())->orderBy('name');
}
static public function isUsable()
@@ -50,18 +73,6 @@ class Server extends BaseModel
return $standaloneDocker->concat($swarmDocker);
}
protected static function booted()
{
static::created(function ($server) {
ServerSetting::create([
'server_id' => $server->id,
]);
});
static::deleting(function ($server) {
$server->settings()->delete();
});
}
public function settings()
{
return $this->hasOne(ServerSetting::class);
@@ -74,12 +85,20 @@ class Server extends BaseModel
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) {

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class Subscription extends Model
{
@@ -14,19 +15,44 @@ class Subscription extends Model
}
public function type()
{
$basic = explode(',', config('coolify.lemon_squeezy_basic_plan_ids'));
$pro = explode(',', config('coolify.lemon_squeezy_pro_plan_ids'));
$ultimate = explode(',', config('coolify.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';
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
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 @@ class Team extends Model implements SendsDiscord, SendsEmail
}
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()
{

View File

@@ -22,6 +22,7 @@ class User extends Authenticatable implements SendsEmail
protected $casts = [
'email_verified_at' => 'datetime',
'force_password_reset' => 'boolean',
'show_boarding' => 'boolean',
];
protected static function boot()
@@ -31,6 +32,7 @@ class User extends Authenticatable implements SendsEmail
$team = [
'name' => $user->name . "'s Team",
'personal_team' => true,
'show_boarding' => true
];
if ($user->id === 0) {
$team['id'] = 0;
@@ -90,36 +92,20 @@ class User extends Authenticatable implements SendsEmail
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 = auth()->user()->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', auth()->user()->currentTeam()->id)->first()->pivot->role;
}
public function resources()
{
$team_id = auth()->user()->currentTeam()->id;
$data = Application::where('team_id', $team_id)->get();
return $data;
return session('currentTeam')->pivot->role;
}
}

View File

@@ -8,6 +8,7 @@ class Webhook extends Model
{
protected $guarded = [];
protected $casts = [
'type' => 'string',
'payload' => 'encrypted',
];
}

View File

@@ -18,15 +18,15 @@ class DeploymentFailed extends Notification implements ShouldQueue
public Application $application;
public string $deployment_uuid;
public ApplicationPreview|null $preview;
public ?ApplicationPreview $preview = null;
public string $application_name;
public string|null $deployment_url = null;
public ?string $deployment_url = null;
public string $project_uuid;
public string $environment_name;
public string|null $fqdn;
public ?string $fqdn = null;
public function __construct(Application $application, string $deployment_uuid, ApplicationPreview|null $preview)
public function __construct(Application $application, string $deployment_uuid, ?ApplicationPreview $preview = null)
{
$this->application = $application;
$this->deployment_uuid = $deployment_uuid;
@@ -44,7 +44,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
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');
@@ -67,9 +67,8 @@ class DeploymentFailed extends Notification implements ShouldQueue
$mail->subject('❌ Deployment failed of ' . $this->application_name . '.');
} else {
$fqdn = $this->preview->fqdn;
$mail->subject('❌ Pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . ' deployment failed.');
$mail->subject('❌ Deployment failed of pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . '.');
}
$mail->view('emails.application-deployment-failed', [
'name' => $this->application_name,
'fqdn' => $fqdn,

View File

@@ -14,6 +14,9 @@ class DiscordChannel
{
$message = $notification->toDiscord($notifiable);
$webhookUrl = $notifiable->routeNotificationForDiscord();
if (!$webhookUrl) {
return;
}
dispatch(new SendMessageToDiscordJob($message, $webhookUrl));
}
}

View File

@@ -9,10 +9,10 @@ use Illuminate\Support\Facades\Mail;
class EmailChannel
{
private bool $isResend = false;
public function send(SendsEmail $notifiable, Notification $notification): void
{
$this->bootConfigs($notifiable);
ray($notification);
$recepients = $notifiable->getRecepients($notification);
if (count($recepients) === 0) {
@@ -20,6 +20,7 @@ class EmailChannel
}
$mailMessage = $notification->toMail($notifiable);
// if ($this->isResend) {
Mail::send(
[],
[],
@@ -28,27 +29,55 @@ class EmailChannel
data_get($notifiable, 'smtp_from_address'),
data_get($notifiable, 'smtp_from_name'),
)
->bcc($recepients)
->to($recepients)
->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);
config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [
"transport" => "smtp",
"host" => data_get($notifiable, 'smtp_host'),
"port" => data_get($notifiable, 'smtp_port'),
"encryption" => data_get($notifiable, 'smtp_encryption'),
"username" => data_get($notifiable, 'smtp_username'),
"password" => $password,
"timeout" => data_get($notifiable, 'smtp_timeout'),
"local_domain" => null,
]);
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', data_get($notifiable, 'resend_api_key'));
}
if (data_get($notifiable, 'smtp_enabled')) {
config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [
"transport" => "smtp",
"host" => data_get($notifiable, 'smtp_host'),
"port" => data_get($notifiable, 'smtp_port'),
"encryption" => data_get($notifiable, 'smtp_encryption'),
"username" => data_get($notifiable, 'smtp_username'),
"password" => data_get($notifiable, 'smtp_password'),
"timeout" => data_get($notifiable, 'smtp_timeout'),
"local_domain" => null,
]);
}
}
}

View File

@@ -4,16 +4,20 @@ namespace App\Notifications\Channels;
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 @@ class TransactionalEmailChannel
}
$this->bootConfigs();
$mailMessage = $notification->toMail($notifiable);
// if ($this->isResend) {
Mail::send(
[],
[],
fn (Message $message) => $message
->from(
data_get($settings, 'smtp_from_address'),
data_get($settings, 'smtp_from_name')
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;
}
}
}

View File

@@ -14,18 +14,19 @@ class BackupFailed extends Notification implements ShouldQueue
{
use Queueable;
public string $message = 'Backup FAILED';
public string $name;
public string $frequency;
public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output)
{
$this->message = "❌ Database backup for {$database->name} with frequency of $backup->frequency was FAILED.\n\nReason: $output";
$this->name = $database->name;
$this->frequency = $backup->frequency;
}
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');
@@ -36,20 +37,23 @@ class BackupFailed extends Notification implements ShouldQueue
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
ray($channels);
return $channels;
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("❌ Backup FAILED for {$this->database->name}");
$mail->line($this->message);
$mail->subject(" [ACTION REQUIRED] Backup FAILED for {$this->database->name}");
$mail->view('emails.backup-failed', [
'name' => $this->name,
'frequency' => $this->frequency,
'output' => $this->output,
]);
return $mail;
}
public function toDiscord(): string
{
return $this->message;
return "❌ Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
}
}

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