Compare commits

..

95 Commits

Author SHA1 Message Date
Andras Bacsai
38f9761b67 Merge pull request #1200 from coollabsio/next
v4.0.0-beta.24
2023-09-06 15:16:44 +02:00
Andras Bacsai
7117ecf634 version++ 2023-09-06 15:16:33 +02:00
Andras Bacsai
522e20f10a proxy updates 2023-09-06 15:00:56 +02:00
Andras Bacsai
ebbce2396c fix: typo 2023-09-06 14:34:35 +02:00
Andras Bacsai
f9a2ff6d90 feat: add discord notifications 2023-09-06 14:31:38 +02:00
Andras Bacsai
df1b9e7319 oops 2023-09-06 12:08:52 +02:00
Andras Bacsai
e7c0c26b32 fix: stripe
add: custom error pages
fix: invititation
feat: new quick login for first users (UX++)
feat: more internal notifications
2023-09-06 12:07:34 +02:00
Andras Bacsai
0dbb8b4420 update name 2023-09-05 16:03:24 +02:00
Andras Bacsai
a4b44bacc1 updates 2023-09-05 15:43:56 +02:00
Andras Bacsai
1338e68b8c wip: backup existing database 2023-09-05 12:14:31 +02:00
Andras Bacsai
f6f4cdde24 new links 2023-09-05 11:53:34 +02:00
Andras Bacsai
3daadf13c6 fix: lowercase image names 2023-09-05 11:05:54 +02:00
Andras Bacsai
cbd5fab2e7 fix 2023-09-05 10:57:49 +02:00
Andras Bacsai
48ccb508f9 update tax collection 2023-09-05 10:49:17 +02:00
Andras Bacsai
67edce0612 update payment webhook 2023-09-05 10:46:36 +02:00
Andras Bacsai
e8a41d7e6e rename command 2023-09-05 10:22:24 +02:00
Andras Bacsai
be1dad03bd fix: do not show system wide git on cloud 2023-09-05 10:03:28 +02:00
Andras Bacsai
31db1db636 next helper image 2023-09-05 08:49:33 +02:00
Andras Bacsai
dca332a688 fix: overlapping apps 2023-09-04 16:59:02 +02:00
Andras Bacsai
35f19ed53f update helper 2023-09-04 16:51:36 +02:00
Andras Bacsai
a5c45ffe90 update pack + nixpacks 2023-09-04 16:50:39 +02:00
Andras Bacsai
b6c3a65d2d update dev build action 2023-09-04 16:48:36 +02:00
Andras Bacsai
220c8211fd update 2023-09-04 16:46:53 +02:00
Andras Bacsai
ffbd04df29 update helper 2023-09-04 16:40:16 +02:00
Andras Bacsai
73c59be865 fix helper 2023-09-04 16:25:18 +02:00
Andras Bacsai
3966abaf80 improve coolify-helper + add test new functionalities 2023-09-04 16:03:11 +02:00
Andras Bacsai
759517316a fix: add docker network to build process 2023-09-04 10:18:30 +02:00
Andras Bacsai
3e3024d47e fix: add navbar for source + keys 2023-09-04 09:44:44 +02:00
Andras Bacsai
517cb77637 update 2023-09-03 11:54:05 +02:00
Andras Bacsai
14bd89a991 update 2023-09-03 11:51:00 +02:00
Andras Bacsai
304de29924 update waitlsit 2023-09-03 11:46:00 +02:00
Andras Bacsai
ab3055150f test distributed application check 2023-09-02 15:53:12 +02:00
Andras Bacsai
6b9c7aa9c5 feat: send request in cloud 2023-09-02 15:37:25 +02:00
Andras Bacsai
040f47b59c fix: show hosted email service, just disable for non pro subs 2023-09-02 13:41:42 +02:00
Andras Bacsai
eac7834083 fix: form address 2023-09-02 13:39:44 +02:00
Andras Bacsai
135a298080 remove line 2023-09-02 13:27:03 +02:00
Andras Bacsai
0065a86371 update seeder 2023-09-01 16:36:41 +02:00
Andras Bacsai
2a842a2f50 Do not schedule autoupdate 2023-09-01 16:30:50 +02:00
Andras Bacsai
231c02e00e version++ 2023-09-01 16:16:35 +02:00
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
210 changed files with 3561 additions and 2449 deletions

View File

@@ -6,6 +6,7 @@
USERID= USERID=
GROUPID= GROUPID=
############################################################################################################ ############################################################################################################
APP_NAME=Coolify-localhost
APP_ID=development APP_ID=development
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=

View File

@@ -0,0 +1,84 @@
name: Coolify Helper Image Development (v4)
on:
push:
branches: [ "next" ]
paths:
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify-helper"
jobs:
amd64:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image and push to registry
uses: docker/build-push-action@v3
with:
no-cache: true
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image and push to registry
uses: docker/build-push-action@v3
with:
no-cache: true
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [ amd64, aarch64 ]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

View File

@@ -2,7 +2,7 @@ name: Coolify Helper Image (v4)
on: on:
push: push:
branches: [ "main", "next" ] branches: [ "main" ]
paths: paths:
- .github/workflows/coolify-helper.yml - .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile - docker/coolify-helper/Dockerfile
@@ -55,7 +55,7 @@ jobs:
file: docker/coolify-helper/Dockerfile file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64 platforms: linux/aarch64
push: true push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:aarch64 tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
merge-manifest: merge-manifest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -78,3 +78,7 @@ jobs:
- name: Create & publish manifest - name: Create & publish manifest
run: | run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}

View File

@@ -3,6 +3,9 @@ name: Development Build (v4)
on: on:
push: push:
branches: ["next"] branches: ["next"]
paths-ignore:
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
@@ -73,4 +76,4 @@ jobs:
- uses: sarisia/actions-status-discord@v1 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

View File

@@ -65,7 +65,7 @@ class StartPostgresql
], ],
'networks' => [ 'networks' => [
$this->database->destination->network => [ $this->database->destination->network => [
'external' => false, 'external' => true,
'name' => $this->database->destination->network, 'name' => $this->database->destination->network,
'attachable' => true, 'attachable' => true,
] ]

View File

@@ -9,15 +9,20 @@ class SaveConfigurationSync
{ {
public function __invoke(Server $server, string $configuration) public function __invoke(Server $server, string $configuration)
{ {
$proxy_path = get_proxy_path(); try {
$docker_compose_yml_base64 = base64_encode($configuration); $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->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save(); $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 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(); $proxy_path = get_proxy_path();
$networks = collect($server->standaloneDockers)->map(function ($docker) { $networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network']; return $docker['network'];

View File

@@ -10,8 +10,14 @@ class InstallDocker
{ {
public function __invoke(Server $server, Team $team) public function __invoke(Server $server, Team $team)
{ {
$dockerVersion = '23.0'; $dockerVersion = '24.0';
$config = base64_encode('{ "live-restore": true }'); $config = base64_encode('{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}');
if (isDev()) { if (isDev()) {
$activity = remote_process([ $activity = remote_process([
"echo ####### Installing Prerequisites...", "echo ####### Installing Prerequisites...",
@@ -37,11 +43,14 @@ class InstallDocker
"docker network create --attachable coolify", "docker network create --attachable coolify",
"echo ####### Done!" "echo ####### Done!"
], $server); ], $server);
StandaloneDocker::create([ $found = StandaloneDocker::where('server_id', $server->id);
'name' => 'coolify', if ($found->count() == 0) {
'network' => 'coolify', StandaloneDocker::create([
'server_id' => $server->id, 'name' => 'coolify',
]); 'network' => 'coolify',
'server_id' => $server->id,
]);
}
} }

View File

@@ -7,9 +7,9 @@ use App\Models\Server;
class UpdateCoolify class UpdateCoolify
{ {
public Server $server; public ?Server $server = null;
public string $latest_version; public ?string $latestVersion = null;
public string $current_version; public ?string $currentVersion = null;
public function __invoke(bool $force) public function __invoke(bool $force)
{ {
@@ -17,13 +17,16 @@ class UpdateCoolify
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
ray('Running InstanceAutoUpdateJob'); ray('Running InstanceAutoUpdateJob');
$localhost_name = 'localhost'; $localhost_name = 'localhost';
$this->server = Server::where('name', $localhost_name)->firstOrFail(); $this->server = Server::where('name', $localhost_name)->first();
$this->latest_version = get_latest_version_of_coolify(); if (!$this->server) {
$this->current_version = config('version'); return;
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) { if ($settings->next_channel) {
ray('next channel enabled'); ray('next channel enabled');
$this->latest_version = 'next'; $this->latestVersion = 'next';
} }
if ($force) { if ($force) {
$this->update(); $this->update();
@@ -31,15 +34,15 @@ class UpdateCoolify
if (!$settings->is_auto_update_enabled) { if (!$settings->is_auto_update_enabled) {
return 'Auto update is disabled'; return 'Auto update is disabled';
} }
if ($this->latest_version === $this->current_version) { if ($this->latestVersion === $this->currentVersion) {
return 'Already on latest version'; return 'Already on latest version';
} }
if (version_compare($this->latest_version, $this->current_version, '<')) { if (version_compare($this->latestVersion, $this->currentVersion, '<')) {
return 'Latest version is lower than current version?!'; return 'Latest version is lower than current version?!';
} }
$this->update(); $this->update();
} }
send_internal_notification('InstanceAutoUpdateJob done to version: ' . $this->latest_version . ' from version: ' . $this->current_version); send_internal_notification('InstanceAutoUpdateJob done to version: ' . $this->latestVersion . ' from version: ' . $this->currentVersion);
} catch (\Exception $th) { } catch (\Exception $th) {
ray('InstanceAutoUpdateJob failed'); ray('InstanceAutoUpdateJob failed');
ray($th->getMessage()); ray($th->getMessage());
@@ -51,7 +54,7 @@ class UpdateCoolify
private function update() private function update()
{ {
if (isDev()) { if (isDev()) {
ray("Running update on local docker container. Updating to $this->latest_version"); ray("Running update on local docker container. Updating to $this->latestVersion");
remote_process([ remote_process([
"sleep 10" "sleep 10"
], $this->server); ], $this->server);
@@ -61,7 +64,7 @@ class UpdateCoolify
ray('Running update on production server'); ray('Running update on production server');
remote_process([ remote_process([
"curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh", "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); ], $this->server);
return; return;
} }

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

@@ -6,20 +6,20 @@ use App\Models\User;
use App\Models\Waitlist; use App\Models\Waitlist;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class InviteFromWaitlist extends Command class WaitlistInvite extends Command
{ {
public Waitlist|null $next_patient = null; public Waitlist|User|null $next_patient = null;
public User|null $new_user = null;
public string|null $password = null; public string|null $password = null;
/** /**
* The name and signature of the console command. * The name and signature of the console command.
* *
* @var string * @var string
*/ */
protected $signature = 'app:invite-from-waitlist {email?}'; protected $signature = 'waitlist:invite {email?} {--only-email}';
/** /**
* The console command description. * The console command description.
@@ -34,7 +34,16 @@ class InviteFromWaitlist extends Command
public function handle() public function handle()
{ {
if ($this->argument('email')) { if ($this->argument('email')) {
$this->next_patient = Waitlist::where('email', $this->argument('email'))->first(); if ($this->option('only-email')) {
$this->next_patient = User::whereEmail($this->argument('email'))->first();
$this->password = Str::password();
$this->next_patient->update([
'password' => Hash::make($this->password),
'force_password_reset' => true,
]);
} else {
$this->next_patient = Waitlist::where('email', $this->argument('email'))->first();
}
if (!$this->next_patient) { if (!$this->next_patient) {
$this->error("{$this->argument('email')} not found in the waitlist."); $this->error("{$this->argument('email')} not found in the waitlist.");
return; return;
@@ -43,6 +52,10 @@ class InviteFromWaitlist extends Command
$this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first(); $this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first();
} }
if ($this->next_patient) { if ($this->next_patient) {
if ($this->option('only-email')) {
$this->send_email();
return;
}
$this->register_user(); $this->register_user();
$this->remove_from_waitlist(); $this->remove_from_waitlist();
$this->send_email(); $this->send_email();
@@ -55,7 +68,7 @@ class InviteFromWaitlist extends Command
$already_registered = User::whereEmail($this->next_patient->email)->first(); $already_registered = User::whereEmail($this->next_patient->email)->first();
if (!$already_registered) { if (!$already_registered) {
$this->password = Str::password(); $this->password = Str::password();
$this->new_user = User::create([ User::create([
'name' => Str::of($this->next_patient->email)->before('@'), 'name' => Str::of($this->next_patient->email)->before('@'),
'email' => $this->next_patient->email, 'email' => $this->next_patient->email,
'password' => Hash::make($this->password), 'password' => Hash::make($this->password),
@@ -73,10 +86,14 @@ class InviteFromWaitlist extends Command
} }
private function send_email() private function send_email()
{ {
ray($this->next_patient->email, $this->password);
$token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password");
$loginLink = route('auth.link', ['token' => $token]);
$mail = new MailMessage(); $mail = new MailMessage();
$mail->view('emails.waitlist-invitation', [ $mail->view('emails.waitlist-invitation', [
'email' => $this->next_patient->email, 'email' => $this->next_patient->email,
'password' => $this->password, 'password' => $this->password,
'loginLink' => $loginLink,
]); ]);
$mail->subject('Congratulations! You are invited to join Coolify Cloud.'); $mail->subject('Congratulations! You are invited to join Coolify Cloud.');
send_user_an_email($mail, $this->next_patient->email); send_user_an_email($mail, $this->next_patient->email);

View File

@@ -2,14 +2,19 @@
namespace App\Console; namespace App\Console;
use App\Jobs\ApplicationContainerStatusJob;
use App\Jobs\CheckResaleLicenseJob; use App\Jobs\CheckResaleLicenseJob;
use App\Jobs\CleanupInstanceStuffsJob; use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\DatabaseBackupJob; use App\Jobs\DatabaseBackupJob;
use App\Jobs\DatabaseContainerStatusJob;
use App\Jobs\DockerCleanupJob; use App\Jobs\DockerCleanupJob;
use App\Jobs\InstanceAutoUpdateJob; use App\Jobs\InstanceAutoUpdateJob;
use App\Jobs\ProxyCheckJob; use App\Jobs\ProxyCheckJob;
use App\Jobs\ResourceStatusJob; use App\Jobs\ResourceStatusJob;
use App\Models\Application;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\StandalonePostgresql;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -17,28 +22,46 @@ class Kernel extends ConsoleKernel
{ {
protected function schedule(Schedule $schedule): void protected function schedule(Schedule $schedule): void
{ {
// $schedule->call(fn() => $this->check_scheduled_backups($schedule))->everyTenSeconds();
if (isDev()) { if (isDev()) {
$schedule->command('horizon:snapshot')->everyMinute(); $schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new ResourceStatusJob)->everyMinute(); // $schedule->job(new ResourceStatusJob)->everyMinute();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes(); $schedule->job(new ProxyCheckJob)->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
// $schedule->job(new CheckResaleLicenseJob)->hourly(); // $schedule->job(new CheckResaleLicenseJob)->hourly();
// $schedule->job(new DockerCleanupJob)->everyOddHour(); $schedule->job(new DockerCleanupJob)->everyOddHour();
// $schedule->job(new InstanceAutoUpdateJob(true))->everyMinute();
} else { } else {
$schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute(); $schedule->job(new CleanupInstanceStuffsJob)->everyTenMinutes()->onOneServer();
$schedule->job(new ResourceStatusJob)->everyMinute(); // $schedule->job(new ResourceStatusJob)->everyMinute()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly(); $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes(); $schedule->job(new ProxyCheckJob)->everyFiveMinutes()->onOneServer();
$schedule->job(new DockerCleanupJob)->everyTenMinutes(); $schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer();
$schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes();
} }
$this->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule); $this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
} }
private function check_resources($schedule)
{
$applications = Application::all();
foreach ($applications as $application) {
$schedule->job(new ApplicationContainerStatusJob($application))->everyMinute()->onOneServer();
}
$postgresqls = StandalonePostgresql::all();
foreach ($postgresqls as $postgresql) {
$schedule->job(new DatabaseContainerStatusJob($postgresql))->everyMinute()->onOneServer();
}
}
private function instance_auto_update($schedule){
if (isDev()) {
return;
}
$settings = InstanceSettings::get();
if ($settings->is_auto_update_enabled) {
$schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes()->onOneServer();
}
}
private function check_scheduled_backups($schedule) private function check_scheduled_backups($schedule)
{ {
ray('check_scheduled_backups'); ray('check_scheduled_backups');
@@ -57,7 +80,7 @@ class Kernel extends ConsoleKernel
} }
$schedule->job(new DatabaseBackupJob( $schedule->job(new DatabaseBackupJob(
backup: $scheduled_backup backup: $scheduled_backup
))->cron($scheduled_backup->frequency); ))->cron($scheduled_backup->frequency)->onOneServer();
} }
} }

View File

@@ -4,6 +4,7 @@ namespace App\Enums;
enum ProxyTypes: string enum ProxyTypes: string
{ {
case NONE = 'NONE';
case TRAEFIK_V2 = 'TRAEFIK_V2'; case TRAEFIK_V2 = 'TRAEFIK_V2';
case NGINX = 'NGINX'; case NGINX = 'NGINX';
case CADDY = 'CADDY'; case CADDY = 'CADDY';

View File

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

View File

@@ -5,39 +5,57 @@ namespace App\Http\Controllers;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Project; use App\Models\Project;
use App\Models\S3Storage; use App\Models\S3Storage;
use App\Models\Server;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\TeamInvitation; use App\Models\TeamInvitation;
use App\Models\User; use App\Models\User;
use App\Models\Waitlist; use Auth;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController; use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
use Throwable; use Throwable;
use Str;
class Controller extends BaseController class Controller extends BaseController
{ {
use AuthorizesRequests, ValidatesRequests; use AuthorizesRequests, ValidatesRequests;
public function waitlist() { public function link()
$waiting_in_line = Waitlist::whereVerified(true)->count(); {
return view('auth.waitlist', [ $token = request()->get('token');
'waiting_in_line' => $waiting_in_line, if ($token) {
]); $decrypted = Crypt::decryptString($token);
$email = Str::of($decrypted)->before('@@@');
$password = Str::of($decrypted)->after('@@@');
$user = User::whereEmail($email)->first();
if (!$user) {
return redirect()->route('login');
}
if (Hash::check($password, $user->password)) {
Auth::login($user);
$team = $user->teams()->first();
session(['currentTeam' => $team]);
return redirect()->route('dashboard');
}
}
return redirect()->route('login')->with('error', 'Invalid credentials.');
} }
public function subscription() public function subscription()
{ {
if (!is_cloud()) { if (!isCloud()) {
abort(404); abort(404);
} }
return view('subscription.show', [ return view('subscription.index', [
'settings' => InstanceSettings::get(), 'settings' => InstanceSettings::get(),
]); ]);
} }
public function license() public function license()
{ {
if (!is_cloud()) { if (!isCloud()) {
abort(404); abort(404);
} }
return view('settings.license', [ return view('settings.license', [
@@ -45,27 +63,12 @@ class Controller extends BaseController
]); ]);
} }
public function force_passoword_reset() { public function force_passoword_reset()
{
return view('auth.force-password-reset'); return view('auth.force-password-reset');
} }
public function dashboard() public function boarding()
{ {
$projects = Project::ownedByCurrentTeam()->get();
$servers = Server::ownedByCurrentTeam()->get();
$s3s = S3Storage::ownedByCurrentTeam()->get();
$resources = 0;
foreach ($projects as $project) {
$resources += $project->applications->count();
$resources += $project->postgresqls->count();
}
return view('dashboard', [
'servers' => $servers->count(),
'projects' => $projects->count(),
'resources' => $resources,
's3s' => $s3s,
]);
}
public function boarding() {
if (currentTeam()->boarding || isDev()) { if (currentTeam()->boarding || isDev()) {
return view('boarding'); return view('boarding');
} else { } else {
@@ -97,7 +100,7 @@ class Controller extends BaseController
if (auth()->user()->isAdminFromSession()) { if (auth()->user()->isAdminFromSession()) {
$invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get(); $invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
} }
return view('team.show', [ return view('team.index', [
'invitations' => $invitations, 'invitations' => $invitations,
]); ]);
} }
@@ -146,7 +149,7 @@ class Controller extends BaseController
if ($diff <= config('constants.invitation.link.expiration')) { if ($diff <= config('constants.invitation.link.expiration')) {
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
$invitation->delete(); $invitation->delete();
return redirect()->route('team.show'); return redirect()->route('team.index');
} else { } else {
$invitation->delete(); $invitation->delete();
abort(401); abort(401);
@@ -168,7 +171,7 @@ class Controller extends BaseController
abort(401); abort(401);
} }
$invitation->delete(); $invitation->delete();
return redirect()->route('team.show'); return redirect()->route('team.index');
} catch (Throwable $th) { } catch (Throwable $th) {
throw $th; throw $th;
} }

View File

@@ -43,6 +43,7 @@ class ProjectController extends Controller
{ {
$type = request()->query('type'); $type = request()->query('type');
$destination_uuid = request()->query('destination'); $destination_uuid = request()->query('destination');
$server = requesT()->query('server');
$project = 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) { if (!$project) {

View File

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

View File

@@ -38,7 +38,7 @@ class Kernel extends HttpKernel
\App\Http\Middleware\VerifyCsrfToken::class, \App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\CheckForcePasswordReset::class, \App\Http\Middleware\CheckForcePasswordReset::class,
\App\Http\Middleware\SubscriptionValid::class, \App\Http\Middleware\IsSubscriptionValid::class,
\App\Http\Middleware\IsBoardingFlow::class, \App\Http\Middleware\IsBoardingFlow::class,
], ],

View File

@@ -1,17 +1,20 @@
<?php <?php
namespace App\Http\Livewire; namespace App\Http\Livewire\Boarding;
use App\Actions\Server\InstallDocker; use App\Actions\Server\InstallDocker;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
class Boarding extends Component class Index extends Component
{ {
public string $currentState = 'welcome'; public string $currentState = 'welcome';
public ?Collection $privateKeys = null;
public ?int $selectedExistingPrivateKey = null;
public ?string $privateKeyType = null; public ?string $privateKeyType = null;
public ?string $privateKey = null; public ?string $privateKey = null;
public ?string $publicKey = null; public ?string $publicKey = null;
@@ -19,6 +22,8 @@ class Boarding extends Component
public ?string $privateKeyDescription = null; public ?string $privateKeyDescription = null;
public ?PrivateKey $createdPrivateKey = null; public ?PrivateKey $createdPrivateKey = null;
public ?Collection $servers = null;
public ?int $selectedExistingServer = null;
public ?string $remoteServerName = null; public ?string $remoteServerName = null;
public ?string $remoteServerDescription = null; public ?string $remoteServerDescription = null;
public ?string $remoteServerHost = null; public ?string $remoteServerHost = null;
@@ -26,6 +31,8 @@ class Boarding extends Component
public ?string $remoteServerUser = 'root'; public ?string $remoteServerUser = 'root';
public ?Server $createdServer = null; public ?Server $createdServer = null;
public Collection|array $projects = [];
public ?int $selectedExistingProject = null;
public ?Project $createdProject = null; public ?Project $createdProject = null;
public function mount() public function mount()
@@ -45,6 +52,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->remoteServerHost = 'coolify-testing-host'; $this->remoteServerHost = 'coolify-testing-host';
} }
} }
public function welcome() {
if (isCloud()) {
return $this->setServerType('remote');
}
$this->currentState = 'select-server-type';
}
public function restartBoarding() public function restartBoarding()
{ {
if ($this->createdServer) { if ($this->createdServer) {
@@ -63,20 +76,62 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
refreshSession(); refreshSession();
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
public function setServer(string $type)
public function setServerType(string $type)
{ {
if ($type === 'localhost') { if ($type === 'localhost') {
$this->createdServer = Server::find(0); $this->createdServer = Server::find(0);
if (!$this->createdServer) { 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->emit('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
} }
$this->currentState = 'select-proxy'; return $this->validateServer();
} elseif ($type === 'remote') { } 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'; $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) public function setPrivateKey(string $type)
{ {
$this->selectedExistingPrivateKey = null;
$this->privateKeyType = $type; $this->privateKeyType = $type;
if ($type === 'create' && !isDev()) { if ($type === 'create' && !isDev()) {
$this->createNewPrivateKey(); $this->createNewPrivateKey();
@@ -115,11 +170,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
'private_key_id' => $this->createdPrivateKey->id, 'private_key_id' => $this->createdPrivateKey->id,
'team_id' => currentTeam()->id 'team_id' => currentTeam()->id
]); ]);
$this->validateServer();
}
public function validateServer() {
try { try {
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->createdServer); ['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->createdServer);
if (!$uptime) { if (!$uptime) {
$this->createdServer->delete();
$this->createdPrivateKey->delete();
throw new \Exception('Server is not reachable.'); throw new \Exception('Server is not reachable.');
} else { } else {
$this->createdServer->settings->update([ $this->createdServer->settings->update([
@@ -127,11 +183,14 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]); ]);
$this->emit('success', 'Server is reachable.'); $this->emit('success', 'Server is reachable.');
} }
if ($dockerVersion) { ray($dockerVersion, $uptime);
if (!$dockerVersion) {
$this->emit('error', 'Docker is not installed on the server.'); $this->emit('error', 'Docker is not installed on the server.');
$this->currentState = 'install-docker'; $this->currentState = 'install-docker';
return; return;
} }
$this->getProxyType();
} catch (\Exception $e) { } catch (\Exception $e) {
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this); return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
} }
@@ -145,13 +204,25 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function selectProxy(string|null $proxyType = null) public function selectProxy(string|null $proxyType = null)
{ {
if (!$proxyType) { if (!$proxyType) {
return $this->currentState = 'create-project'; return $this->getProjects();
} }
$this->createdServer->proxy->type = $proxyType; $this->createdServer->proxy->type = $proxyType;
$this->createdServer->proxy->status = 'exited'; $this->createdServer->proxy->status = 'exited';
$this->createdServer->save(); $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'; $this->currentState = 'create-project';
} }
public function selectExistingProject() {
$this->createdProject = Project::find($this->selectedExistingProject);
$this->currentState = 'create-resource';
}
public function createNewProject() public function createNewProject()
{ {
$this->createdProject = Project::create([ $this->createdProject = Project::create([
@@ -168,7 +239,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
[ [
'project_uuid' => $this->createdProject->uuid, 'project_uuid' => $this->createdProject->uuid,
'environment_name' => 'production', 'environment_name' => 'production',
'server'=> $this->createdServer->id,
] ]
); );
} }
@@ -176,6 +247,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
{ {
$this->privateKeyName = generate_random_name(); $this->privateKeyName = generate_random_name();
$this->privateKeyDescription = 'Created by Coolify'; $this->privateKeyDescription = 'Created by Coolify';
['private' => $this->privateKey, 'public'=> $this->publicKey] = generateSSHKey(); ['private' => $this->privateKey, 'public' => $this->publicKey] = generateSSHKey();
}
public function render()
{
return view('livewire.boarding.index')->layout('layouts.boarding');
} }
} }

View File

@@ -0,0 +1,41 @@
<?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 getIptables()
// {
// $servers = Server::ownedByCurrentTeam()->get();
// foreach ($servers as $server) {
// checkRequiredCommands($server);
// $iptables = instant_remote_process(['docker run --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c "iptables -L -n | jc --iptables"'], $server);
// ray($iptables);
// }
// }
public function render()
{
return view('livewire.dashboard');
}
}

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

@@ -18,22 +18,26 @@ class ForcePasswordReset extends Component
'password' => 'required|min:8', 'password' => 'required|min:8',
'password_confirmation' => 'required|same:password', 'password_confirmation' => 'required|same:password',
]; ];
public function mount() { public function mount()
{
$this->email = auth()->user()->email; $this->email = auth()->user()->email;
} }
public function submit() { public function submit()
{
try { try {
$this->rateLimit(10); $this->rateLimit(10);
$this->validate(); $this->validate();
$firstLogin = auth()->user()->created_at == auth()->user()->updated_at;
auth()->user()->forceFill([ auth()->user()->forceFill([
'password' => Hash::make($this->password), 'password' => Hash::make($this->password),
'force_password_reset' => false, 'force_password_reset' => false,
])->save(); ])->save();
auth()->logout(); if ($firstLogin) {
return redirect()->route('login')->with('status', 'Your initial password has been set.'); send_internal_notification('First login for ' . auth()->user()->email);
} catch(\Exception $e) { }
return general_error_handler(err:$e, that:$this); return redirect()->route('dashboard');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
} }
} }
} }

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Livewire;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Illuminate\Notifications\Messages\MailMessage;
use Livewire\Component;
use Route;
class Help extends Component
{
use WithRateLimiting;
public string $description;
public string $subject;
public ?string $path = null;
protected $rules = [
'description' => 'required|min:10',
'subject' => 'required|min:3'
];
public function mount()
{
$this->path = Route::current()->uri();
if (isDev()) {
$this->description = "I'm having trouble with {$this->path}";
$this->subject = "Help with {$this->path}";
}
}
public function submit()
{
try {
$this->rateLimit(1, 60);
$this->validate();
$subscriptionType = auth()->user()?->subscription?->type() ?? 'unknown';
$debug = "Route: {$this->path}";
$mail = new MailMessage();
$mail->view(
'emails.help',
[
'description' => $this->description,
'debug' => $debug
]
);
$mail->subject("[HELP - {$subscriptionType}]: {$this->subject}");
send_user_an_email($mail, 'hi@coollabs.io');
$this->emit('success', 'Your message has been sent successfully. We will get in touch with you as soon as possible.');
} catch (\Exception $e) {
return general_error_handler($e, $this);
}
}
public function render()
{
return view('livewire.help')->layout('layouts.app');
}
}

View File

@@ -8,26 +8,30 @@ use Livewire\Component;
class DiscordSettings extends Component class DiscordSettings extends Component
{ {
public Team $model; public Team $team;
protected $rules = [ protected $rules = [
'model.discord_enabled' => 'nullable|boolean', 'team.discord_enabled' => 'nullable|boolean',
'model.discord_webhook_url' => 'required|url', 'team.discord_webhook_url' => 'required|url',
'model.discord_notifications_test' => 'nullable|boolean', 'team.discord_notifications_test' => 'nullable|boolean',
'model.discord_notifications_deployments' => 'nullable|boolean', 'team.discord_notifications_deployments' => 'nullable|boolean',
'model.discord_notifications_status_changes' => 'nullable|boolean', 'team.discord_notifications_status_changes' => 'nullable|boolean',
'model.discord_notifications_database_backups' => 'nullable|boolean', 'team.discord_notifications_database_backups' => 'nullable|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'model.discord_webhook_url' => 'Discord Webhook', 'team.discord_webhook_url' => 'Discord Webhook',
]; ];
public function mount()
{
$this->team = auth()->user()->currentTeam();
}
public function instantSave() public function instantSave()
{ {
try { try {
$this->submit(); $this->submit();
} catch (\Exception $e) { } catch (\Exception $e) {
ray($e->getMessage()); ray($e->getMessage());
$this->model->discord_enabled = false; $this->team->discord_enabled = false;
$this->validate(); $this->validate();
} }
} }
@@ -41,8 +45,8 @@ class DiscordSettings extends Component
public function saveModel() public function saveModel()
{ {
$this->model->save(); $this->team->save();
if (is_a($this->model, Team::class)) { if (is_a($this->team, Team::class)) {
refreshSession(); refreshSession();
} }
$this->emit('success', 'Settings saved.'); $this->emit('success', 'Settings saved.');
@@ -50,7 +54,7 @@ class DiscordSettings extends Component
public function sendTestNotification() public function sendTestNotification()
{ {
$this->model->notify(new Test); $this->team->notify(new Test());
$this->emit('success', 'Test notification sent.'); $this->emit('success', 'Test notification sent.');
} }
} }

View File

@@ -6,55 +6,144 @@ use App\Models\InstanceSettings;
use App\Models\Team; use App\Models\Team;
use App\Notifications\Test; use App\Notifications\Test;
use Livewire\Component; use Livewire\Component;
use Log;
class EmailSettings extends Component class EmailSettings extends Component
{ {
public Team $model; public Team $team;
public string $emails; public string $emails;
public bool $sharedEmailEnabled = false;
protected $rules = [ protected $rules = [
'model.smtp_enabled' => 'nullable|boolean', 'team.smtp_enabled' => 'nullable|boolean',
'model.smtp_from_address' => 'required|email', 'team.smtp_from_address' => 'required|email',
'model.smtp_from_name' => 'required', 'team.smtp_from_name' => 'required',
'model.smtp_recipients' => 'nullable', 'team.smtp_recipients' => 'nullable',
'model.smtp_host' => 'required', 'team.smtp_host' => 'required',
'model.smtp_port' => 'required', 'team.smtp_port' => 'required',
'model.smtp_encryption' => 'nullable', 'team.smtp_encryption' => 'nullable',
'model.smtp_username' => 'nullable', 'team.smtp_username' => 'nullable',
'model.smtp_password' => 'nullable', 'team.smtp_password' => 'nullable',
'model.smtp_timeout' => 'nullable', 'team.smtp_timeout' => 'nullable',
'model.smtp_notifications_test' => 'nullable|boolean', 'team.smtp_notifications_test' => 'nullable|boolean',
'model.smtp_notifications_deployments' => 'nullable|boolean', 'team.smtp_notifications_deployments' => 'nullable|boolean',
'model.smtp_notifications_status_changes' => 'nullable|boolean', 'team.smtp_notifications_status_changes' => 'nullable|boolean',
'model.smtp_notifications_database_backups' => '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 = [ protected $validationAttributes = [
'model.smtp_from_address' => 'From Address', 'team.smtp_from_address' => 'From Address',
'model.smtp_from_name' => 'From Name', 'team.smtp_from_name' => 'From Name',
'model.smtp_recipients' => 'Recipients', 'team.smtp_recipients' => 'Recipients',
'model.smtp_host' => 'Host', 'team.smtp_host' => 'Host',
'model.smtp_port' => 'Port', 'team.smtp_port' => 'Port',
'model.smtp_encryption' => 'Encryption', 'team.smtp_encryption' => 'Encryption',
'model.smtp_username' => 'Username', 'team.smtp_username' => 'Username',
'model.smtp_password' => 'Password', 'team.smtp_password' => 'Password',
'team.smtp_timeout' => 'Timeout',
'team.resend_enabled' => 'Resend Enabled',
'team.resend_api_key' => 'Resend API Key',
]; ];
public function mount() public function mount()
{ {
$this->decrypt(); $this->team = auth()->user()->currentTeam();
['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits;
$this->emails = auth()->user()->email; $this->emails = auth()->user()->email;
} }
public function submitFromFields()
private function decrypt()
{ {
if (data_get($this->model, 'smtp_password')) { try {
try { $this->resetErrorBag();
$this->model->smtp_password = decrypt($this->model->smtp_password); $this->validate([
} catch (\Exception $e) { '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() public function copyFromInstanceSettings()
{ {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
@@ -72,55 +161,22 @@ class EmailSettings extends Component
'smtp_password' => $settings->smtp_password, 'smtp_password' => $settings->smtp_password,
'smtp_timeout' => $settings->smtp_timeout, 'smtp_timeout' => $settings->smtp_timeout,
]); ]);
$this->decrypt();
if (is_a($team, Team::class)) {
refreshSession();
}
$this->model = $team;
$this->emit('success', 'Settings saved.');
} else {
$this->emit('error', 'Instance SMTP settings are not enabled.');
}
}
public function sendTestNotification()
{
$this->model->notify(new Test($this->emails));
$this->emit('success', 'Test Email sent successfully.');
}
public function instantSave()
{
try {
$this->submit();
} catch (\Exception $e) {
$this->model->smtp_enabled = false;
$this->validate();
}
}
public function submit()
{
$this->resetErrorBag();
$this->validate();
if ($this->model->smtp_password) {
$this->model->smtp_password = encrypt($this->model->smtp_password);
} else {
$this->model->smtp_password = null;
}
$this->model->smtp_recipients = str_replace(' ', '', $this->model->smtp_recipients);
$this->saveModel();
}
public function saveModel()
{
$this->model->save();
$this->decrypt();
if (is_a($this->model, Team::class)) {
refreshSession(); refreshSession();
$this->team = $team;
$this->emit('success', 'Settings saved.');
return;
} }
$this->emit('success', 'Settings saved.'); if ($settings->resend_enabled) {
$team = currentTeam();
$team->update([
'resend_enabled' => $settings->resend_enabled,
'resend_api_key' => $settings->resend_api_key,
]);
refreshSession();
$this->team = $team;
$this->emit('success', 'Settings saved.');
return;
}
$this->emit('error', 'Instance SMTP/Resend settings are not enabled.');
} }
} }

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Http\Livewire\Notifications;
use App\Models\Team;
use App\Notifications\Test;
use Livewire\Component;
class TelegramSettings extends Component
{
public Team $team;
protected $rules = [
'team.telegram_enabled' => 'nullable|boolean',
'team.telegram_token' => 'required|string',
'team.telegram_chat_id' => 'required|string',
'team.telegram_notifications_test' => 'nullable|boolean',
'team.telegram_notifications_deployments' => 'nullable|boolean',
'team.telegram_notifications_status_changes' => 'nullable|boolean',
'team.telegram_notifications_database_backups' => 'nullable|boolean',
];
protected $validationAttributes = [
'team.telegram_token' => 'Token',
'team.telegram_chat_id' => 'Chat ID',
];
public function mount()
{
$this->team = auth()->user()->currentTeam();
}
public function instantSave()
{
try {
$this->submit();
} catch (\Exception $e) {
ray($e->getMessage());
$this->team->telegram_enabled = false;
$this->validate();
}
}
public function submit()
{
$this->resetErrorBag();
$this->validate();
$this->saveModel();
}
public function saveModel()
{
$this->team->save();
if (is_a($this->team, Team::class)) {
refreshSession();
}
$this->emit('success', 'Settings saved.');
}
public function sendTestNotification()
{
$this->team->notify(new Test());
$this->emit('success', 'Test notification sent.');
}
}

View File

@@ -27,7 +27,7 @@ class Change extends Component
if ($this->private_key->isEmpty()) { if ($this->private_key->isEmpty()) {
$this->private_key->delete(); $this->private_key->delete();
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get(); currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
return redirect()->route('private-key.all'); return redirect()->route('security.private-key.index');
} }
$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.'); $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.');
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@@ -7,15 +7,19 @@ use App\Models\GithubApp;
use App\Models\Project; use App\Models\Project;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use App\Traits\SaveFromRedirect;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Livewire\Component; use Livewire\Component;
use Route;
class GithubPrivateRepository extends Component class GithubPrivateRepository extends Component
{ {
use SaveFromRedirect;
public $current_step = 'github_apps'; public $current_step = 'github_apps';
public $github_apps; public $github_apps;
public GithubApp $github_app; public GithubApp $github_app;
public $parameters; public $parameters;
public $currentRoute;
public $query; public $query;
public $type; public $type;
@@ -36,14 +40,30 @@ class GithubPrivateRepository extends Component
public string|null $publish_directory = null; public string|null $publish_directory = null;
protected int $page = 1; 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() public function mount()
{ {
$this->currentRoute = Route::currentRouteName();
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->query = request()->query(); $this->query = request()->query();
$this->repositories = $this->branches = collect(); $this->repositories = $this->branches = collect();
$this->github_apps = GithubApp::private(); $this->github_apps = GithubApp::private();
} }
public function loadRepositories($github_app_id) public function loadRepositories($github_app_id)
{ {
$this->repositories = collect(); $this->repositories = collect();

View File

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

View File

@@ -3,45 +3,79 @@
namespace App\Http\Livewire\Project\New; namespace App\Http\Livewire\Project\New;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Countable; use Countable;
use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
use Route;
class Select extends Component class Select extends Component
{ {
public $current_step = 'type'; public $current_step = 'type';
public ?int $server = null;
public string $type; public string $type;
public string $server_id; public string $server_id;
public string $destination_uuid; public string $destination_uuid;
public Countable|array|Server $servers; public Countable|array|Server $servers;
public $destinations = []; public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = [];
public array $parameters; public array $parameters;
public ?string $existingPostgresqlUrl = null;
protected $queryString = [
'server',
];
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
if (isDev()) {
$this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432';
}
} }
public function set_type(string $type) // public function addExistingPostgresql()
// {
// try {
// instantCommand("psql {$this->existingPostgresqlUrl} -c 'SELECT 1'");
// $this->emit('success', 'Successfully connected to the database.');
// } catch (\Exception $e) {
// return general_error_handler($e, $this);
// }
// }
public function setType(string $type)
{ {
$this->type = $type; $this->type = $type;
if ($type === "existing-postgresql") {
$this->current_step = $type;
return;
}
if (count($this->servers) === 1) { if (count($this->servers) === 1) {
$server = $this->servers->first(); $server = $this->servers->first();
$this->set_server($server); $this->setServer($server);
if (count($server->destinations()) === 1) { if (count($server->destinations()) === 1) {
$this->set_destination($server->destinations()->first()->uuid); $this->setDestination($server->destinations()->first()->uuid);
}
}
if (!is_null($this->server)) {
$foundServer = $this->servers->where('id', $this->server)->first();
if ($foundServer) {
return $this->setServer($foundServer);
} }
} }
$this->current_step = 'servers'; $this->current_step = 'servers';
} }
public function set_server(Server $server) public function setServer(Server $server)
{ {
$this->server_id = $server->id; $this->server_id = $server->id;
$this->destinations = $server->destinations(); $this->standaloneDockers = $server->standaloneDockers;
$this->swarmDockers = $server->swarmDockers;
$this->current_step = 'destinations'; $this->current_step = 'destinations';
} }
public function set_destination(string $destination_uuid) public function setDestination(string $destination_uuid)
{ {
$this->destination_uuid = $destination_uuid; $this->destination_uuid = $destination_uuid;
redirect()->route('project.resources.new', [ redirect()->route('project.resources.new', [

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\Actions\Server\InstallDocker;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
class Form extends Component class Form extends Component
{ {
use AuthorizesRequests;
public Server $server; public Server $server;
public $uptime; public $uptime;
public $dockerVersion; public $dockerVersion;
@@ -64,14 +66,20 @@ class Form extends Component
public function delete() public function delete()
{ {
if (!$this->server->isEmpty()) { try {
$this->emit('error', 'Server has defined resources. Please delete them first.'); $this->authorize('delete', $this->server);
return; 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() public function submit()
{ {
$this->validate(); $this->validate();

View File

@@ -2,6 +2,8 @@
namespace App\Http\Livewire\Server\New; namespace App\Http\Livewire\Server\New;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -67,6 +69,11 @@ class ByIp extends Component
'port' => $this->port, 'port' => $this->port,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'private_key_id' => $this->private_key_id, 'private_key_id' => $this->private_key_id,
'proxy' => [
"type" => ProxyTypes::TRAEFIK_V2->value,
"status" => ProxyStatus::EXITED->value,
]
]); ]);
$server->settings->is_part_of_swarm = $this->is_part_of_swarm; $server->settings->is_part_of_swarm = $this->is_part_of_swarm;
$server->settings->save(); $server->settings->save();

View File

@@ -12,7 +12,7 @@ class Proxy extends Component
{ {
public Server $server; public Server $server;
public ProxyTypes $selectedProxy = ProxyTypes::TRAEFIK_V2; public ?string $selectedProxy = null;
public $proxy_settings = null; public $proxy_settings = null;
public string|null $redirect_url = null; public string|null $redirect_url = null;
@@ -20,6 +20,7 @@ class Proxy extends Component
public function mount() public function mount()
{ {
$this->selectedProxy = $this->server->proxy->type;
$this->redirect_url = $this->server->proxy->redirect_url; $this->redirect_url = $this->server->proxy->redirect_url;
} }
@@ -35,11 +36,12 @@ class Proxy extends Component
$this->emit('proxyStatusUpdated'); $this->emit('proxyStatusUpdated');
} }
public function select_proxy(ProxyTypes $proxy_type) public function select_proxy($proxy_type)
{ {
$this->server->proxy->type = $proxy_type; $this->server->proxy->type = $proxy_type;
$this->server->proxy->status = 'exited'; $this->server->proxy->status = 'exited';
$this->server->save(); $this->server->save();
$this->selectedProxy = $this->server->proxy->type;
$this->emit('proxyStatusUpdated'); $this->emit('proxyStatusUpdated');
} }

View File

@@ -12,10 +12,12 @@ class Status extends Component
public function get_status() public function get_status()
{ {
dispatch_sync(new ProxyContainerStatusJob( if (data_get($this->server,'settings.is_usable')) {
server: $this->server dispatch_sync(new ProxyContainerStatusJob(
)); server: $this->server
$this->server->refresh(); ));
$this->emit('proxyStatusUpdated'); $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 Livewire\Component;
use Masmerise\Toaster\Toaster; use Masmerise\Toaster\Toaster;
class PrivateKey extends Component class ShowPrivateKey extends Component
{ {
public Server $server; public Server $server;
public $privateKeys; public $privateKeys;

View File

@@ -20,6 +20,9 @@ class Email extends Component
'settings.smtp_timeout' => 'nullable', 'settings.smtp_timeout' => 'nullable',
'settings.smtp_from_address' => 'required|email', 'settings.smtp_from_address' => 'required|email',
'settings.smtp_from_name' => 'required', 'settings.smtp_from_name' => 'required',
'settings.resend_enabled' => 'nullable|boolean',
'settings.resend_api_key' => 'nullable'
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'settings.smtp_from_address' => 'From Address', 'settings.smtp_from_address' => 'From Address',
@@ -30,48 +33,68 @@ class Email extends Component
'settings.smtp_encryption' => 'Encryption', 'settings.smtp_encryption' => 'Encryption',
'settings.smtp_username' => 'Username', 'settings.smtp_username' => 'Username',
'settings.smtp_password' => 'Password', 'settings.smtp_password' => 'Password',
'settings.smtp_timeout' => 'Timeout',
'settings.resend_api_key' => 'Resend API Key'
]; ];
public function mount() public function mount()
{ {
$this->decrypt();
$this->emails = auth()->user()->email; $this->emails = auth()->user()->email;
} }
private function decrypt() public function submitFromFields() {
{ try {
if (data_get($this->settings, 'smtp_password')) { $this->resetErrorBag();
try { $this->validate([
$this->settings->smtp_password = decrypt($this->settings->smtp_password); 'settings.smtp_from_address' => 'required|email',
} catch (\Exception $e) { '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() public function instantSave()
{ {
try { try {
$this->submit(); $this->submit();
$this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) { } catch (\Exception $e) {
$this->settings->smtp_enabled = false; return general_error_handler($e, $this);
$this->validate();
} }
} }
public function submit() public function submit()
{ {
$this->resetErrorBag(); try {
$this->validate(); $this->resetErrorBag();
if ($this->settings->smtp_password) { $this->validate([
$this->settings->smtp_password = encrypt($this->settings->smtp_password); 'settings.smtp_host' => 'required',
} else { 'settings.smtp_port' => 'required|numeric',
$this->settings->smtp_password = null; '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() public function sendTestNotification()

View File

@@ -37,9 +37,13 @@ class Change extends Component
public function mount() 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->parameters = get_route_parameters();
$this->is_system_wide = $this->github_app->is_system_wide;
} }
public function submit() public function submit()

View File

@@ -32,16 +32,22 @@ class Create extends Component
"custom_port" => 'required|int', "custom_port" => 'required|int',
"is_system_wide" => 'required|bool', "is_system_wide" => 'required|bool',
]); ]);
$github_app = GithubApp::create([ $payload = [
'name' => $this->name, 'name' => $this->name,
'organization' => $this->organization, 'organization' => $this->organization,
'api_url' => $this->api_url, 'api_url' => $this->api_url,
'html_url' => $this->html_url, 'html_url' => $this->html_url,
'custom_user' => $this->custom_user, 'custom_user' => $this->custom_user,
'custom_port' => $this->custom_port, 'custom_port' => $this->custom_port,
'is_system_wide' => $this->is_system_wide,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
]); ];
if (isCloud()) {
$payload['is_system_wide'] = $this->is_system_wide;
}
$github_app = GithubApp::create($payload);
if (session('from')) {
session(['from' => session('from') + ['source_id' => $github_app->id]]);
}
redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]); redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]);
} catch (\Exception $e) { } catch (\Exception $e) {
return general_error_handler(err: $e, that: $this); return general_error_handler(err: $e, that: $this);

View File

@@ -47,9 +47,12 @@ class PricingPlans extends Component
'tax_id_collection' => [ 'tax_id_collection' => [
'enabled' => true, 'enabled' => true,
], ],
'automatic_tax' => [
'enabled' => true,
],
'mode' => 'subscription', 'mode' => 'subscription',
'success_url' => route('subscription.success'), 'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.show',['cancelled' => true]), 'cancel_url' => route('subscription.index', ['cancelled' => true]),
]; ];
$customer = currentTeam()->subscription?->stripe_customer_id ?? null; $customer = currentTeam()->subscription?->stripe_customer_id ?? null;
if ($customer) { if ($customer) {

View File

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

View File

@@ -25,6 +25,6 @@ class Delete extends Component
}); });
refreshSession(); refreshSession();
return redirect()->route('team.show'); return redirect()->route('team.index');
} }
} }

View File

@@ -28,7 +28,6 @@ class Form extends Component
try { try {
$this->team->save(); $this->team->save();
refreshSession(); refreshSession();
$this->emit('reloadWindow');
} catch (\Throwable $th) { } catch (\Throwable $th) {
return general_error_handler($th, $this); return general_error_handler($th, $this);
} }

View File

@@ -14,6 +14,7 @@ class Invitations extends Component
{ {
TeamInvitation::find($invitation_id)->delete(); TeamInvitation::find($invitation_id)->delete();
$this->refreshInvitations(); $this->refreshInvitations();
$this->emit('success', 'Invitation revoked.');
} }
public function refreshInvitations() public function refreshInvitations()

View File

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

View File

@@ -16,6 +16,12 @@ class CheckForcePasswordReset
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
if (auth()->user()) { if (auth()->user()) {
if ($request->path() === 'auth/link') {
auth()->logout();
request()->session()->invalidate();
request()->session()->regenerateToken();
return $next($request);
}
$force_password_reset = auth()->user()->force_password_reset; $force_password_reset = auth()->user()->force_password_reset;
if ($force_password_reset) { if ($force_password_reset) {
if ($request->routeIs('auth.force-password-reset') || $request->path() === 'livewire/message/force-password-reset') { if ($request->routeIs('auth.force-password-reset') || $request->path() === 'livewire/message/force-password-reset') {

View File

@@ -15,7 +15,7 @@ class IsBoardingFlow
*/ */
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
// ray('IsBoardingFlow Middleware'); ray()->showQueries()->color('orange');
if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) { if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect('boarding'); return redirect('boarding');
} }

View File

@@ -6,14 +6,14 @@ use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
class SubscriptionValid class IsSubscriptionValid
{ {
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
if (isInstanceAdmin()) { if (isInstanceAdmin()) {
return $next($request); return $next($request);
} }
if (!auth()->user() || !is_cloud()) { if (!auth()->user() || !isCloud()) {
if ($request->path() === 'subscription') { if ($request->path() === 'subscription') {
return redirect('/'); return redirect('/');
} else { } else {

View File

@@ -20,6 +20,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -65,6 +66,7 @@ class ApplicationDeploymentJob implements ShouldQueue
private $log_model; private $log_model;
private Collection $saved_outputs; private Collection $saved_outputs;
public $tries = 1;
public function __construct(int $application_deployment_queue_id) public function __construct(int $application_deployment_queue_id)
{ {
ray()->clearScreen(); ray()->clearScreen();
@@ -174,8 +176,8 @@ class ApplicationDeploymentJob implements ShouldQueue
$this->execute_in_builder("echo '$dockerfile_base64' | base64 -d > $this->workdir/Dockerfile") $this->execute_in_builder("echo '$dockerfile_base64' | base64 -d > $this->workdir/Dockerfile")
], ],
); );
$this->build_image_name = "{$this->application->git_repository}:build"; $this->build_image_name = Str::lower("{$this->application->git_repository}:build");
$this->production_image_name = "{$this->application->uuid}:latest"; $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
@@ -199,8 +201,8 @@ class ApplicationDeploymentJob implements ShouldQueue
$tag = $tag->substr(0, 128); $tag = $tag->substr(0, 128);
} }
$this->build_image_name = "{$this->application->git_repository}:{$tag}-build"; $this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
$this->production_image_name = "{$this->application->uuid}:{$tag}"; $this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
if (!$this->force_rebuild) { if (!$this->force_rebuild) {
@@ -235,7 +237,7 @@ class ApplicationDeploymentJob implements ShouldQueue
} }
private function health_check() private function health_check()
{ {
ray('New container name: ',$this->container_name); ray('New container name: ', $this->container_name);
if ($this->container_name) { if ($this->container_name) {
$counter = 0; $counter = 0;
$this->execute_remote_command( $this->execute_remote_command(
@@ -257,7 +259,7 @@ class ApplicationDeploymentJob implements ShouldQueue
); );
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo 'New application version health check status: {$this->saved_outputs->get('health_check')}'" "echo 'New version health check status: {$this->saved_outputs->get('health_check')}'"
], ],
); );
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) { if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
@@ -275,8 +277,8 @@ class ApplicationDeploymentJob implements ShouldQueue
} }
private function deploy_pull_request() private function deploy_pull_request()
{ {
$this->build_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}-build"; $this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
$this->production_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}"; $this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->execute_remote_command([ $this->execute_remote_command([
"echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->application->git_repository}:{$this->application->git_branch}.'", "echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->application->git_repository}:{$this->application->git_branch}.'",
@@ -297,12 +299,19 @@ class ApplicationDeploymentJob implements ShouldQueue
private function prepare_builder_image() private function prepare_builder_image()
{ {
$pull = "--pull=always";
if (isDev()) {
$pull = "--pull=never";
}
$helperImage = config('coolify.helper_image');
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo -n 'Pulling latest version of the builder image (ghcr.io/coollabsio/coolify-helper).'", "echo -n 'Pulling helper image from $helperImage.'",
], ],
[ [
"docker run --pull=always -d --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/coollabsio/coolify-helper", $runCommand,
"hidden" => true, "hidden" => true,
], ],
[ [
@@ -487,7 +496,7 @@ class ApplicationDeploymentJob implements ShouldQueue
], ],
'networks' => [ 'networks' => [
$this->destination->network => [ $this->destination->network => [
'external' => false, 'external' => true,
'name' => $this->destination->network, 'name' => $this->destination->network,
'attachable' => true, 'attachable' => true,
] ]
@@ -647,12 +656,12 @@ class ApplicationDeploymentJob implements ShouldQueue
private function build_image() private function build_image()
{ {
$this->execute_remote_command([ $this->execute_remote_command([
"echo -n 'Building docker image.'", "echo -n 'Building docker image for your application.'",
]); ]);
if ($this->application->settings->is_static) { if ($this->application->settings->is_static) {
$this->execute_remote_command([ $this->execute_remote_command([
$this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true $this->execute_in_builder("docker build --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
]); ]);
$dockerfile = base64_encode("FROM {$this->application->static_image} $dockerfile = base64_encode("FROM {$this->application->static_image}
@@ -685,12 +694,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_in_builder("echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf") $this->execute_in_builder("echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf")
], ],
[ [
$this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true $this->execute_in_builder("docker build --network host -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
] ]
); );
} else { } else {
$this->execute_remote_command([ $this->execute_remote_command([
$this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true $this->execute_in_builder("docker build --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]); ]);
} }
} }
@@ -699,7 +708,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
{ {
if ($this->currently_running_container_name) { if ($this->currently_running_container_name) {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Removing old application version.'"], ["echo -n 'Removing old version of your application.'"],
[$this->execute_in_builder("docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true], [$this->execute_in_builder("docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true],
); );
} }

View File

@@ -37,7 +37,7 @@ class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique
private function cleanup_waitlist() 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) { foreach ($waitlist as $item) {
$item->delete(); $item->delete();
} }

View File

@@ -77,7 +77,7 @@ class DatabaseBackupJob implements ShouldQueue
$ip = Str::slug($this->server->ip); $ip = Str::slug($this->server->ip);
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip"; $this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
} }
$this->backup_file = "/dumpall-" . Carbon::now()->timestamp . ".sql"; $this->backup_file = "/pg_dump-" . Carbon::now()->timestamp . ".dump";
$this->backup_location = $this->backup_dir . $this->backup_file; $this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([ $this->backup_log = ScheduledDatabaseBackupExecution::create([
@@ -107,7 +107,7 @@ class DatabaseBackupJob implements ShouldQueue
try { try {
ray($this->backup_dir); ray($this->backup_dir);
$commands[] = "mkdir -p " . $this->backup_dir; $commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name pg_dumpall -U {$this->database->postgres_user} > $this->backup_location"; $commands[] = "docker exec $this->container_name pg_dump -Fc -U {$this->database->postgres_user} > $this->backup_location";
$this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = instant_remote_process($commands, $this->server);

View File

@@ -2,64 +2,88 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server; use App\Models\Server;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
class DockerCleanupJob implements ShouldQueue class DockerCleanupJob implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 500; public $timeout = 500;
public ?string $dockerRootFilesystem = null;
public ?int $usageBefore = null;
/** public function middleware(): array
* Create a new job instance. {
*/ return [
(new WithoutOverlapping("dockerimagejobs"))->shared(),
];
}
public function __construct() public function __construct()
{ {
//
} }
/**
* Execute the job.
*/
public function handle(): void public function handle(): void
{ {
$queue = ApplicationDeploymentQueue::where('status', '==', 'in_progress')->get();
if ($queue->count() > 0) {
ray('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping')->color('orange');
return;
}
try { try {
ray()->showQueries()->color('orange');
$servers = Server::all(); $servers = Server::all();
foreach ($servers as $server) { foreach ($servers as $server) {
if (isDev()) { if (
$docker_root_filesystem = "/"; !$server->settings->is_reachable && !$server->settings->is_usable
} else { ) {
$docker_root_filesystem = instant_remote_process(['stat --printf=%m $(docker info --format "{{json .DockerRootDir}}" |sed \'s/"//g\')'], $server); continue;
} }
$disk_percentage_before = $this->get_disk_usage($server, $docker_root_filesystem); if (isDev()) {
if ($disk_percentage_before >= $server->settings->cleanup_after_percentage) { $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 image prune -af'], $server);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server); instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server);
instant_remote_process(['docker builder prune -af'], $server); instant_remote_process(['docker builder prune -af'], $server);
$disk_percentage_after = $this->get_disk_usage($server, $docker_root_filesystem); $usageAfter = $this->getFilesystemUsage($server);
if ($disk_percentage_after < $disk_percentage_before) { if ($usageAfter < $this->usageBefore) {
ray('Saved ' . ($disk_percentage_before - $disk_percentage_after) . '% disk space on ' . $server->name); 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) { } catch (\Exception $e) {
send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage()); send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage());
ray($e->getMessage()); ray($e->getMessage())->color('orange');
throw $e; 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); return instant_remote_process(["df '{$this->dockerRootFilesystem}'| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $server, false);
$mount_point = collect(data_get($disk_usage, 'disks'))->where('mount', $docker_root_filesystem)->first();
ray($mount_point);
return Str::of(data_get($mount_point, 'use%'))->trim()->replace('%', '')->value();
} }
} }

View File

@@ -33,8 +33,9 @@ class ProxyCheckJob implements ShouldQueue
if ($status === 'running') { if ($status === 'running') {
continue; continue;
} }
// $server->team->notify(new ProxyStoppedNotification($server)); if (data_get($server, 'proxy.type')) {
resolve(StartProxy::class)($server); resolve(StartProxy::class)($server);
}
} }
} catch (\Throwable $th) { } catch (\Throwable $th) {
ray($th->getMessage()); ray($th->getMessage());

View File

@@ -39,9 +39,9 @@ class ProxyContainerStatusJob implements ShouldQueue, ShouldBeUnique
public function handle(): void public function handle(): void
{ {
try { try {
$container = getContainerStatus(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: true); $container = getContainerStatus(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: false);
$status = data_get($container, 'State.Status'); $status = data_get($container, 'State.Status');
if (data_get($this->server,'proxy.status') !== $status) { if ($status && data_get($this->server, 'proxy.status') !== $status) {
$this->server->proxy->status = $status; $this->server->proxy->status = $status;
if ($this->server->proxy->status === 'running') { if ($this->server->proxy->status === 'running') {
$traefik = $container['Config']['Labels']['org.opencontainers.image.title']; $traefik = $container['Config']['Labels']['org.opencontainers.image.title'];

View File

@@ -3,6 +3,8 @@
namespace App\Jobs; namespace App\Jobs;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server; use App\Models\Server;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@@ -27,6 +29,11 @@ class ProxyStartJob implements ShouldQueue
if ($status === 'running') { if ($status === 'running') {
return; 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); resolve(StartProxy::class)($this->server);
} catch (\Throwable $th) { } catch (\Throwable $th) {
send_internal_notification('ProxyStartJob failed with: ' . $th->getMessage()); send_internal_notification('ProxyStartJob failed with: ' . $th->getMessage());

View File

@@ -37,7 +37,7 @@ class SendConfirmationForWaitlistJob implements ShouldQueue
$mail->subject('You are on the waitlist!'); $mail->subject('You are on the waitlist!');
send_user_an_email($mail, $this->email); send_user_an_email($mail, $this->email);
} catch (\Throwable $th) { } 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()); ray($th->getMessage());
throw $th; throw $th;
} }

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Str;
class SendMessageToTelegramJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 5;
/**
* The maximum number of unhandled exceptions to allow before failing.
*/
public int $maxExceptions = 3;
public function __construct(
public string $text,
public array $buttons,
public string $token,
public string $chatId,
) {
}
/**
* Execute the job.
*/
public function handle(): void
{
$url = 'https://api.telegram.org/bot' . $this->token . '/sendMessage';
$inlineButtons = [];
if (!empty($this->buttons)) {
foreach ($this->buttons as $button) {
$buttonUrl = data_get($button, 'url');
if ($buttonUrl && Str::contains($buttonUrl, 'http://localhost')) {
$buttonUrl = str_replace('http://localhost', config('app.url'), $buttonUrl);
}
$inlineButtons[] = [
'text' => $button['text'],
'url' => $buttonUrl,
];
}
}
$payload = [
'parse_mode' => 'markdown',
'reply_markup' => json_encode([
'inline_keyboard' => [
[...$inlineButtons],
],
]),
'chat_id' => $this->chatId,
'text' => $this->text,
];
ray($payload);
$response = Http::post($url, $payload);
if ($response->failed()) {
throw new \Exception('Telegram notification failed with ' . $response->status() . ' status code.' . $response->body());
}
}
}

View File

@@ -9,7 +9,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Stripe\Stripe;
class SubscriptionInvoiceFailedJob implements ShouldQueue class SubscriptionInvoiceFailedJob implements ShouldQueue
{ {

View File

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

View File

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

View File

@@ -33,6 +33,9 @@ class Server extends BaseModel
}); });
static::deleting(function ($server) { static::deleting(function ($server) {
$server->destinations()->each(function ($destination) {
$destination->delete();
});
$server->settings()->delete(); $server->settings()->delete();
}); });
} }
@@ -70,8 +73,6 @@ class Server extends BaseModel
return $standaloneDocker->concat($swarmDocker); return $standaloneDocker->concat($swarmDocker);
} }
public function settings() public function settings()
{ {
return $this->hasOne(ServerSetting::class); return $this->hasOne(ServerSetting::class);
@@ -84,12 +85,20 @@ class Server extends BaseModel
public function isEmpty() public function isEmpty()
{ {
if ($this->applications()->count() === 0) { $applications = $this->applications()->count() === 0;
$databases = $this->databases()->count() === 0;
if ($applications && $databases) {
return true; return true;
} }
return false; return false;
} }
public function databases() {
return $this->destinations()->map(function ($standaloneDocker) {
$postgresqls = $standaloneDocker->postgresqls;
return $postgresqls?->concat([]) ?? collect([]);
})->flatten();
}
public function applications() public function applications()
{ {
return $this->destinations()->map(function ($standaloneDocker) { return $this->destinations()->map(function ($standaloneDocker) {

View File

@@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class Subscription extends Model class Subscription extends Model
{ {
@@ -14,19 +15,44 @@ class Subscription extends Model
} }
public function type() public function type()
{ {
$basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids')); if (isLemon()) {
$pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids')); $basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids'));
$ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_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; $subscription = $this->lemon_variant_id;
if (in_array($subscription, $basic)) { if (in_array($subscription, $basic)) {
return 'basic'; return 'basic';
}
if (in_array($subscription, $pro)) {
return 'pro';
}
if (in_array($subscription, $ultimate)) {
return 'ultimate';
}
} }
if (in_array($subscription, $pro)) { if (isStripe()) {
return 'pro'; if (!$this->stripe_plan_id) {
} return 'unknown';
if (in_array($subscription, $ultimate)) { }
return 'ultimate'; $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'; return 'unknown';
} }

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Notifications\Channels\SendsDiscord; use App\Notifications\Channels\SendsDiscord;
use App\Notifications\Channels\SendsEmail; use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@@ -14,6 +15,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
protected $guarded = []; protected $guarded = [];
protected $casts = [ protected $casts = [
'personal_team' => 'boolean', 'personal_team' => 'boolean',
'smtp_password' => 'encrypted',
'resend_api_key' => 'encrypted',
]; ];
public function routeNotificationForDiscord() public function routeNotificationForDiscord()
@@ -21,6 +24,14 @@ class Team extends Model implements SendsDiscord, SendsEmail
return data_get($this, 'discord_webhook_url', null); return data_get($this, 'discord_webhook_url', null);
} }
public function routeNotificationForTelegram()
{
return [
"token" => data_get($this, 'telegram_token', null),
"chat_id" => data_get($this, 'telegram_chat_id', null)
];
}
public function getRecepients($notification) public function getRecepients($notification)
{ {
$recipients = data_get($notification, 'emails', null); $recipients = data_get($notification, 'emails', null);
@@ -30,6 +41,27 @@ class Team extends Model implements SendsDiscord, SendsEmail
} }
return explode(',', $recipients); 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() public function members()
{ {

View File

@@ -32,6 +32,7 @@ class User extends Authenticatable implements SendsEmail
$team = [ $team = [
'name' => $user->name . "'s Team", 'name' => $user->name . "'s Team",
'personal_team' => true, 'personal_team' => true,
'show_boarding' => true
]; ];
if ($user->id === 0) { if ($user->id === 0) {
$team['id'] = 0; $team['id'] = 0;
@@ -91,29 +92,20 @@ class User extends Authenticatable implements SendsEmail
return $found_root_team->count() > 0; return $found_root_team->count() > 0;
} }
public function personalTeam()
{
return $this->teams()->where('personal_team', true)->first();
}
public function currentTeam() public function currentTeam()
{ {
return $this->teams()->where('team_id', session('currentTeam')->id)->first(); return Team::find(session('currentTeam')->id);
} }
public function otherTeams() public function otherTeams()
{ {
$team_id = currentTeam()->id; return auth()->user()->teams->filter(function ($team) {
return auth()->user()->teams->filter(function ($team) use ($team_id) { return $team->id != currentTeam()->id;
return $team->id != $team_id;
}); });
} }
public function role() public function role()
{ {
if ($this->teams()->where('team_id', 0)->first()) { return session('currentTeam')->pivot->role;
return 'admin';
}
return $this->teams()->where('team_id', currentTeam()->id)->first()->pivot->role;
} }
} }

View File

@@ -18,15 +18,15 @@ class DeploymentFailed extends Notification implements ShouldQueue
public Application $application; public Application $application;
public string $deployment_uuid; public string $deployment_uuid;
public ApplicationPreview|null $preview; public ?ApplicationPreview $preview = null;
public string $application_name; public string $application_name;
public string|null $deployment_url = null; public ?string $deployment_url = null;
public string $project_uuid; public string $project_uuid;
public string $environment_name; 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->application = $application;
$this->deployment_uuid = $deployment_uuid; $this->deployment_uuid = $deployment_uuid;
@@ -43,19 +43,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
$channels = []; return setNotificationChannels($notifiable, 'deployments');
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_deployments');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_deployments');
if ($isEmailEnabled && $isSubscribedToEmailEvent) {
$channels[] = EmailChannel::class;
}
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
return $channels;
} }
public function toMail(): MailMessage public function toMail(): MailMessage
@@ -67,9 +55,8 @@ class DeploymentFailed extends Notification implements ShouldQueue
$mail->subject('❌ Deployment failed of ' . $this->application_name . '.'); $mail->subject('❌ Deployment failed of ' . $this->application_name . '.');
} else { } else {
$fqdn = $this->preview->fqdn; $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', [ $mail->view('emails.application-deployment-failed', [
'name' => $this->application_name, 'name' => $this->application_name,
'fqdn' => $fqdn, 'fqdn' => $fqdn,
@@ -90,4 +77,19 @@ class DeploymentFailed extends Notification implements ShouldQueue
} }
return $message; return $message;
} }
public function toTelegram(): array
{
if ($this->preview) {
$message = '❌ Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: ';
} else {
$message = '❌ Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): ';
}
return [
"message" => $message,
"buttons" => [
"text" => "View Deployment Logs",
"url" => $this->deployment_url
],
];
}
} }

View File

@@ -43,19 +43,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
$channels = []; return setNotificationChannels($notifiable, 'deployments');
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_deployments');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_deployments');
if ($isEmailEnabled && $isSubscribedToEmailEvent) {
$channels[] = EmailChannel::class;
}
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
return $channels;
} }
public function toMail(): MailMessage public function toMail(): MailMessage
@@ -99,4 +87,34 @@ class DeploymentSuccess extends Notification implements ShouldQueue
} }
return $message; return $message;
} }
public function toTelegram(): array
{
if ($this->preview) {
$message = '✅ New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . '';
if ($this->preview->fqdn) {
$buttons[] = [
"text" => "Open Application",
"url" => $this->preview->fqdn
];
}
} else {
$message = '✅ New version successfully deployed of ' . $this->application_name . '';
if ($this->fqdn) {
$buttons[] = [
"text" => "Open Application",
"url" => $this->fqdn
];
}
}
$buttons[] = [
"text" => "Deployment logs",
"url" => $this->deployment_url
];
return [
"message" => $message,
"buttons" => [
...$buttons
],
];
}
} }

View File

@@ -37,19 +37,7 @@ class StatusChanged extends Notification implements ShouldQueue
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
$channels = []; return setNotificationChannels($notifiable, 'status_changes');
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_status_changes');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_status_changes');
if ($isEmailEnabled && $isSubscribedToEmailEvent) {
$channels[] = EmailChannel::class;
}
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
return $channels;
} }
public function toMail(): MailMessage public function toMail(): MailMessage
@@ -70,7 +58,20 @@ class StatusChanged extends Notification implements ShouldQueue
$message = '⛔ ' . $this->application_name . ' has been stopped. $message = '⛔ ' . $this->application_name . ' has been stopped.
'; ';
$message .= '[Application URL](' . $this->application_url . ')'; $message .= '[Open Application in Coolify](' . $this->application_url . ')';
return $message; return $message;
} }
public function toTelegram(): array
{
$message = '⛔ ' . $this->application_name . ' has been stopped.';
return [
"message" => $message,
"buttons" => [
[
"text" => "Open Application in Coolify",
"url" => $this->application_url
]
],
];
}
} }

View File

@@ -6,7 +6,6 @@ use Exception;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
class EmailChannel class EmailChannel
{ {
@@ -24,11 +23,7 @@ class EmailChannel
[], [],
[], [],
fn (Message $message) => $message fn (Message $message) => $message
->from( ->to($recepients)
data_get($notifiable, 'smtp_from_address'),
data_get($notifiable, 'smtp_from_name'),
)
->bcc($recepients)
->subject($mailMessage->subject) ->subject($mailMessage->subject)
->html((string)$mailMessage->render()) ->html((string)$mailMessage->render())
); );
@@ -36,13 +31,20 @@ class EmailChannel
private function bootConfigs($notifiable): void private function bootConfigs($notifiable): void
{ {
$password = data_get($notifiable, 'smtp_password'); if (data_get($notifiable, 'use_instance_email_settings')) {
if ($password) $password = decrypt($password); $type = set_transanctional_email_settings();
if (!$type) {
if (Str::contains(data_get($notifiable, 'smtp_host'),'resend.com')) { throw new Exception('No email settings found.');
}
return;
}
config()->set('mail.from.address', data_get($notifiable, 'smtp_from_address'));
config()->set('mail.from.name', data_get($notifiable, 'smtp_from_name'));
if (data_get($notifiable, 'resend_enabled')) {
config()->set('mail.default', 'resend'); config()->set('mail.default', 'resend');
config()->set('resend.api_key', $password); config()->set('resend.api_key', data_get($notifiable, 'resend_api_key'));
} else { }
if (data_get($notifiable, 'smtp_enabled')) {
config()->set('mail.default', 'smtp'); config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [ config()->set('mail.mailers.smtp', [
"transport" => "smtp", "transport" => "smtp",
@@ -50,7 +52,7 @@ class EmailChannel
"port" => data_get($notifiable, 'smtp_port'), "port" => data_get($notifiable, 'smtp_port'),
"encryption" => data_get($notifiable, 'smtp_encryption'), "encryption" => data_get($notifiable, 'smtp_encryption'),
"username" => data_get($notifiable, 'smtp_username'), "username" => data_get($notifiable, 'smtp_username'),
"password" => $password, "password" => data_get($notifiable, 'smtp_password'),
"timeout" => data_get($notifiable, 'smtp_timeout'), "timeout" => data_get($notifiable, 'smtp_timeout'),
"local_domain" => null, "local_domain" => null,
]); ]);

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Notifications\Channels;
interface SendsTelegram
{
public function routeNotificationForTelegram();
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Notifications\Channels;
use App\Jobs\SendMessageToTelegramJob;
class TelegramChannel
{
public function send($notifiable, $notification): void
{
$data = $notification->toTelegram($notifiable);
$telegramData = $notifiable->routeNotificationForTelegram();
$message = data_get($data, 'message');
$buttons = data_get($data, 'buttons', []);
ray($message, $buttons);
$telegramToken = data_get($telegramData, 'token');
$chatId = data_get($telegramData, 'chat_id');
if (!$telegramToken || !$chatId || !$message) {
throw new \Exception('Telegram token, chat id and message are required');
}
dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId));
}
}

View File

@@ -4,16 +4,19 @@ namespace App\Notifications\Channels;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\User; use App\Models\User;
use Exception;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Log;
class TransactionalEmailChannel class TransactionalEmailChannel
{ {
public function send(User $notifiable, Notification $notification): void public function send(User $notifiable, Notification $notification): void
{ {
$settings = InstanceSettings::get(); $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; return;
} }
$email = $notifiable->email; $email = $notifiable->email;
@@ -26,10 +29,6 @@ class TransactionalEmailChannel
[], [],
[], [],
fn (Message $message) => $message fn (Message $message) => $message
->from(
data_get($settings, 'smtp_from_address'),
data_get($settings, 'smtp_from_name')
)
->to($email) ->to($email)
->subject($mailMessage->subject) ->subject($mailMessage->subject)
->html((string)$mailMessage->render()) ->html((string)$mailMessage->render())
@@ -38,6 +37,9 @@ class TransactionalEmailChannel
private function bootConfigs(): void private function bootConfigs(): void
{ {
set_transanctional_email_settings(); $type = set_transanctional_email_settings();
if (!$type) {
throw new Exception('No email settings found.');
}
} }
} }

View File

@@ -14,42 +14,41 @@ class BackupFailed extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public string $message = 'Backup FAILED'; public string $name;
public string $frequency;
public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output) 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 public function via(object $notifiable): array
{ {
$channels = []; return setNotificationChannels($notifiable, 'database_backups');
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups');
if ($isEmailEnabled && $isSubscribedToEmailEvent) {
$channels[] = EmailChannel::class;
}
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
ray($channels);
return $channels;
} }
public function toMail(): MailMessage public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject("❌ Backup FAILED for {$this->database->name}"); $mail->subject(" [ACTION REQUIRED] Backup FAILED for {$this->database->name}");
$mail->line($this->message); $mail->view('emails.backup-failed', [
'name' => $this->name,
'frequency' => $this->frequency,
'output' => $this->output,
]);
return $mail; return $mail;
} }
public function toDiscord(): string public function toDiscord(): string
{ {
return $this->message; return "❌ Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
}
public function toTelegram(): array
{
$message = "❌ Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
return [
"message" => $message,
];
} }
} }

View File

@@ -14,41 +14,40 @@ class BackupSuccess extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public string $message = 'Backup Success'; public string $name;
public string $frequency;
public function __construct(ScheduledDatabaseBackup $backup, public $database) public function __construct(ScheduledDatabaseBackup $backup, public $database)
{ {
$this->message = "✅ Database backup for {$database->name} with frequency of $backup->frequency was successful."; $this->name = $database->name;
$this->frequency = $backup->frequency;
} }
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
$channels = []; return setNotificationChannels($notifiable, 'database_backups');
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups');
if ($isEmailEnabled && $isSubscribedToEmailEvent) {
$channels[] = EmailChannel::class;
}
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
return $channels;
} }
public function toMail(): MailMessage public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject("✅ Backup success for {$this->database->name}"); $mail->subject("✅ Backup successfully done for {$this->database->name}");
$mail->line($this->message); $mail->view('emails.backup-success', [
'name' => $this->name,
'frequency' => $this->frequency,
]);
return $mail; return $mail;
} }
public function toDiscord(): string public function toDiscord(): string
{ {
return $this->message; return "✅ Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
}
public function toTelegram(): array
{
$message = "✅ Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
return [
"message" => $message,
];
} }
} }

View File

@@ -3,6 +3,7 @@
namespace App\Notifications\Internal; namespace App\Notifications\Internal;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
@@ -12,16 +13,22 @@ class GeneralNotification extends Notification implements ShouldQueue
use Queueable; use Queueable;
public function __construct(public string $message) public function __construct(public string $message)
{} {
}
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
$channels[] = DiscordChannel::class; return [TelegramChannel::class, DiscordChannel::class];
return $channels;
} }
public function toDiscord(): string public function toDiscord(): string
{ {
return $this->message; return $this->message;
} }
public function toTelegram(): array
{
return [
"message" => $this->message,
];
}
} }

View File

@@ -22,19 +22,7 @@ class NotReachable extends Notification implements ShouldQueue
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
$channels = []; return setNotificationChannels($notifiable, 'status_changes');
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_status_changes');
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_status_changes');
// if ($isEmailEnabled && $isSubscribedToEmailEvent) {
// $channels[] = EmailChannel::class;
// }
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
return $channels;
} }
public function toMail(): MailMessage public function toMail(): MailMessage
@@ -55,4 +43,10 @@ class NotReachable extends Notification implements ShouldQueue
$message = '⛔ Server \'' . $this->server->name . '\' is unreachable (could be a temporary issue). If you receive this more than twice in a row, please check your server.'; $message = '⛔ Server \'' . $this->server->name . '\' is unreachable (could be a temporary issue). If you receive this more than twice in a row, please check your server.';
return $message; return $message;
} }
public function toTelegram(): array
{
return [
"message" => '⛔ Server \'' . $this->server->name . '\' is unreachable (could be a temporary issue). If you receive this more than twice in a row, please check your server.'
];
}
} }

View File

@@ -4,6 +4,7 @@ namespace App\Notifications;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@@ -19,24 +20,13 @@ class Test extends Notification implements ShouldQueue
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
$channels = []; return setNotificationChannels($notifiable, 'test');
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
if ($isDiscordEnabled && empty($this->emails)) {
$channels[] = DiscordChannel::class;
}
if ($isEmailEnabled && !empty($this->emails)) {
$channels[] = EmailChannel::class;
}
return $channels;
} }
public function toMail(): MailMessage public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject("Coolify Test Notification"); $mail->subject("Test Email");
$mail->view('emails.test'); $mail->view('emails.test');
return $mail; return $mail;
} }
@@ -48,4 +38,16 @@ class Test extends Notification implements ShouldQueue
$message .= '[Go to your dashboard](' . base_url() . ')'; $message .= '[Go to your dashboard](' . base_url() . ')';
return $message; return $message;
} }
public function toTelegram(): array
{
return [
"message" => 'This is a test Telegram notification from Coolify.',
"buttons" => [
[
"text" => "Go to your dashboard",
"url" => 'https://coolify.io'
]
],
];
}
} }

View File

@@ -20,16 +20,19 @@ class InvitationLink extends Notification implements ShouldQueue
return [TransactionalEmailChannel::class]; return [TransactionalEmailChannel::class];
} }
public function toMail(User $user): MailMessage public function __construct(public User $user)
{ {
$invitation = TeamInvitation::whereEmail($user->email)->first(); }
public function toMail(): MailMessage
{
$invitation = TeamInvitation::whereEmail($this->user->email)->first();
$invitation_team = Team::find($invitation->team->id); $invitation_team = Team::find($invitation->team->id);
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject('Invitation for ' . $invitation_team->name); $mail->subject('Invitation for ' . $invitation_team->name);
$mail->view('emails.invitation-link', [ $mail->view('emails.invitation-link', [
'team' => $invitation_team->name, 'team' => $invitation_team->name,
'email' => $user->email, 'email' => $this->user->email,
'invitation_link' => $invitation->link, 'invitation_link' => $invitation->link,
]); ]);
return $mail; return $mail;

View File

@@ -31,24 +31,11 @@ class ResetPassword extends Notification
public function via($notifiable) public function via($notifiable)
{ {
if ($this->settings->smtp_enabled) { $type = set_transanctional_email_settings();
$password = data_get($this->settings, 'smtp_password'); if (!$type) {
if ($password) $password = decrypt($password); throw new \Exception('No email settings found.');
config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [
"transport" => "smtp",
"host" => data_get($this->settings, 'smtp_host'),
"port" => data_get($this->settings, 'smtp_port'),
"encryption" => data_get($this->settings, 'smtp_encryption'),
"username" => data_get($this->settings, 'smtp_username'),
"password" => $password,
"timeout" => data_get($this->settings, 'smtp_timeout'),
"local_domain" => null,
]);
return ['mail'];
} }
throw new \Exception('SMTP is not enabled'); return ['mail'];
} }
public function toMail($notifiable) public function toMail($notifiable)
@@ -63,10 +50,6 @@ class ResetPassword extends Notification
protected function buildMailMessage($url) protected function buildMailMessage($url)
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->from(
data_get($this->settings, 'smtp_from_address'),
data_get($this->settings, 'smtp_from_name'),
);
$mail->subject('Reset Password'); $mail->subject('Reset Password');
$mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]); $mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]);
return $mail; return $mail;

View File

@@ -24,7 +24,7 @@ class Test extends Notification implements ShouldQueue
public function toMail(): MailMessage public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject('Test Notification'); $mail->subject('Test Email');
$mail->view('emails.test'); $mail->view('emails.test');
return $mail; return $mail;
} }

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\Server;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class ServerPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Server $server): bool
{
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return true;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Server $server): bool
{
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Server $server): bool
{
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Server $server): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Server $server): bool
{
return false;
}
}

View File

@@ -43,22 +43,16 @@ class FortifyServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
Fortify::createUsersUsing(CreateNewUser::class); Fortify::createUsersUsing(CreateNewUser::class);
Fortify::registerView(function () { Fortify::registerView(function () {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
$waiting_in_line = Waitlist::whereVerified(true)->count();
if (!$settings->is_registration_enabled) { if (!$settings->is_registration_enabled) {
return redirect()->route('login'); return redirect()->route('login');
} }
if (config('coolify.waitlist')) { if (config('coolify.waitlist')) {
return view('auth.waitlist',[ return redirect()->route('waitlist.index');
'waiting_in_line' => $waiting_in_line,
]);
} else { } else {
return view('auth.register',[ return view('auth.register');
'waiting_in_line' => $waiting_in_line,
]);
} }
}); });

View File

@@ -41,6 +41,7 @@ trait ExecuteRemoteCommand
$remote_command = generate_ssh_command($private_key_location, $ip, $user, $port, $command); $remote_command = generate_ssh_command($private_key_location, $ip, $user, $port, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) { $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) {
$output = Str::of($output)->trim();
$new_log_entry = [ $new_log_entry = [
'command' => $command, 'command' => $command,
'output' => $output, 'output' => $output,

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Traits;
use Illuminate\Support\Collection;
trait SaveFromRedirect
{
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);
}
}

View File

@@ -15,7 +15,7 @@ class Button extends Component
public bool $disabled = false, public bool $disabled = false,
public bool $isModal = false, public bool $isModal = false,
public bool $noStyle = false, public bool $noStyle = false,
public string|null $modalId = null, public ?string $modalId = null,
public string $defaultClass = "btn btn-primary btn-sm font-normal text-white normal-case no-animation rounded border-none" public string $defaultClass = "btn btn-primary btn-sm font-normal text-white normal-case no-animation rounded border-none"
) { ) {
if ($this->noStyle) { if ($this->noStyle) {
@@ -23,9 +23,6 @@ class Button extends Component
} }
} }
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string public function render(): View|Closure|string
{ {
return view('components.forms.button'); return view('components.forms.button');

View File

@@ -17,7 +17,7 @@ class Checkbox extends Component
public string|null $value = null, public string|null $value = null,
public string|null $label = null, public string|null $label = null,
public string|null $helper = null, public string|null $helper = null,
public bool $instantSave = false, public string|bool $instantSave = false,
public bool $disabled = false, public bool $disabled = false,
public string $defaultClass = "toggle toggle-xs toggle-warning rounded disabled:bg-coolgray-200 disabled:opacity-50 placeholder:text-neutral-700" public string $defaultClass = "toggle toggle-xs toggle-warning rounded disabled:bg-coolgray-200 disabled:opacity-50 placeholder:text-neutral-700"
) { ) {

View File

@@ -58,9 +58,11 @@ function format_docker_envs_to_json($rawOutput)
} }
function getApplicationContainerStatus(Application $application) { function getApplicationContainerStatus(Application $application) {
$server = $application->destination->server; $server = data_get($application,'destination.server');
$id = $application->id; $id = $application->id;
if (!$server) {
return 'exited';
}
$containers = getCurrentApplicationContainerStatus($server, $id); $containers = getCurrentApplicationContainerStatus($server, $id);
if ($containers->count() > 0) { if ($containers->count() > 0) {
$status = data_get($containers[0], 'State', 'exited'); $status = data_get($containers[0], 'State', 'exited');
@@ -84,7 +86,7 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
function generateApplicationContainerName(string $uuid, int $pull_request_id = 0) function generateApplicationContainerName(string $uuid, int $pull_request_id = 0)
{ {
$now = now()->format('YmdHis'); $now = now()->format('Hisu');
if ($pull_request_id !== 0 && $pull_request_id !== null) { if ($pull_request_id !== 0 && $pull_request_id !== null) {
return $uuid . '-pr-' . $pull_request_id . '-' . $now; return $uuid . '-pr-' . $pull_request_id . '-' . $now;
} else { } else {

View File

@@ -16,11 +16,6 @@ use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Sleep; use Illuminate\Support\Sleep;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
/**
* Run a Remote Process, which SSH's asynchronously into a machine to run the command(s).
* @TODO Change 'root' to 'coolify' when it's able to run Docker commands without sudo
*
*/
function remote_process( function remote_process(
array $command, array $command,
Server $server, Server $server,
@@ -82,6 +77,7 @@ function generate_ssh_command(string $private_key_location, string $server_ip, s
if ($isMux && config('coolify.mux_enabled')) { if ($isMux && config('coolify.mux_enabled')) {
$ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r '; $ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r ';
} }
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$ssh_command .= "-i {$private_key_location} " $ssh_command .= "-i {$private_key_location} "
. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' . '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
. '-o PasswordAuthentication=no ' . '-o PasswordAuthentication=no '
@@ -97,7 +93,18 @@ function generate_ssh_command(string $private_key_location, string $server_ip, s
return $ssh_command; return $ssh_command;
} }
function instantCommand(string $command, $throwError = true) {
$process = Process::run($command);
$output = trim($process->output());
$exitCode = $process->exitCode();
if ($exitCode !== 0) {
if (!$throwError) {
return null;
}
throw new \RuntimeException($process->errorOutput(), $exitCode);
}
return $output;
}
function instant_remote_process(array $command, Server $server, $throwError = true, $repeat = 1) function instant_remote_process(array $command, Server $server, $throwError = true, $repeat = 1)
{ {
$command_string = implode("\n", $command); $command_string = implode("\n", $command);
@@ -167,17 +174,23 @@ function validateServer(Server $server)
{ {
try { try {
refresh_server_connection($server->privateKey); refresh_server_connection($server->privateKey);
$uptime = instant_remote_process(['uptime'], $server); $uptime = instant_remote_process(['uptime'], $server, false);
if (!$uptime) { if (!$uptime) {
$uptime = 'Server not reachable.'; $server->settings->is_reachable = false;
throw new \Exception('Server not reachable.'); return [
"uptime" => null,
"dockerVersion" => null,
];
} }
$server->settings->is_reachable = true; $server->settings->is_reachable = true;
$dockerVersion = instant_remote_process(['docker version|head -2|grep -i version'], $server, false); $dockerVersion = instant_remote_process(['docker version|head -2|grep -i version'], $server, false);
if (!$dockerVersion) { if (!$dockerVersion) {
$dockerVersion = 'Not installed.'; $dockerVersion = null;
throw new \Exception('Docker not installed.'); return [
"uptime" => $uptime,
"dockerVersion" => null,
];
} }
$server->settings->is_usable = true; $server->settings->is_usable = true;
return [ return [
@@ -215,3 +228,29 @@ function check_server_connection(Server $server)
$server->save(); $server->save();
} }
} }
function checkRequiredCommands(Server $server)
{
$commands = collect(["jq", "jc"]);
foreach ($commands as $command) {
$commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false);
if ($commandFound) {
ray($command . ' found');
continue;
}
try {
instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'apt update && apt install -y {$command}'"], $server);
} catch (\Exception $e) {
ray('could not install ' . $command);
ray($e);
break;
}
$commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false);
if ($commandFound) {
ray($command . ' found');
continue;
}
ray('could not install ' . $command);
break;
}
}

View File

@@ -2,6 +2,9 @@
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Team; use App\Models\Team;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Internal\GeneralNotification; use App\Notifications\Internal\GeneralNotification;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException; use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
@@ -52,12 +55,13 @@ function showBoarding(): bool
} }
function refreshSession(): void function refreshSession(): void
{ {
$team = currentTeam(); $team = Team::find(currentTeam()->id);
session(['currentTeam' => $team]); session(['currentTeam' => $team]);
} }
function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed
{ {
try { try {
ray($err);
ray('ERROR OCCURRED: ' . $err->getMessage()); ray('ERROR OCCURRED: ' . $err->getMessage());
if ($err instanceof QueryException) { if ($err instanceof QueryException) {
if ($err->errorInfo[0] === '23505') { if ($err->errorInfo[0] === '23505') {
@@ -70,6 +74,9 @@ function general_error_handler(Throwable | null $err = null, $that = null, $isJs
} elseif ($err instanceof TooManyRequestsException) { } elseif ($err instanceof TooManyRequestsException) {
throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds."); throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds.");
} else { } else {
if ($err->getMessage() === 'This action is unauthorized.') {
return redirect()->route('dashboard')->with('error', $customErrorMessage ?? $err->getMessage());
}
throw new Exception($customErrorMessage ?? $err->getMessage()); throw new Exception($customErrorMessage ?? $err->getMessage());
} }
} catch (Throwable $error) { } catch (Throwable $error) {
@@ -117,10 +124,11 @@ function generateSSHKey()
$key = RSA::createKey(); $key = RSA::createKey();
return [ return [
'private' => $key->toString('PKCS1'), 'private' => $key->toString('PKCS1'),
'public' => $key->getPublicKey()->toString('OpenSSH',['comment' => 'coolify-generated-ssh-key']) 'public' => $key->getPublicKey()->toString('OpenSSH', ['comment' => 'coolify-generated-ssh-key'])
]; ];
} }
function formatPrivateKey(string $privateKey) { function formatPrivateKey(string $privateKey)
{
$privateKey = trim($privateKey); $privateKey = trim($privateKey);
if (!str_ends_with($privateKey, "\n")) { if (!str_ends_with($privateKey, "\n")) {
$privateKey .= "\n"; $privateKey .= "\n";
@@ -135,30 +143,36 @@ function generate_application_name(string $git_repository, string $git_branch):
function is_transactional_emails_active(): bool function is_transactional_emails_active(): bool
{ {
return data_get(InstanceSettings::get(), 'smtp_enabled'); return isEmailEnabled(InstanceSettings::get());
} }
function set_transanctional_email_settings(InstanceSettings | null $settings = null): void function set_transanctional_email_settings(InstanceSettings | null $settings = null): string|null
{ {
if (!$settings) { if (!$settings) {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
} }
$password = data_get($settings, 'smtp_password'); config()->set('mail.from.address', data_get($settings, 'smtp_from_address'));
if (isset($password)) { config()->set('mail.from.name', data_get($settings, 'smtp_from_name'));
$password = decrypt($password); if (data_get($settings, 'resend_enabled')) {
config()->set('mail.default', 'resend');
config()->set('resend.api_key', data_get($settings, 'resend_api_key'));
return 'resend';
} }
if (data_get($settings, 'smtp_enabled')) {
config()->set('mail.default', 'smtp'); config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [ config()->set('mail.mailers.smtp', [
"transport" => "smtp", "transport" => "smtp",
"host" => data_get($settings, 'smtp_host'), "host" => data_get($settings, 'smtp_host'),
"port" => data_get($settings, 'smtp_port'), "port" => data_get($settings, 'smtp_port'),
"encryption" => data_get($settings, 'smtp_encryption'), "encryption" => data_get($settings, 'smtp_encryption'),
"username" => data_get($settings, 'smtp_username'), "username" => data_get($settings, 'smtp_username'),
"password" => $password, "password" => data_get($settings, 'smtp_password'),
"timeout" => data_get($settings, 'smtp_timeout'), "timeout" => data_get($settings, 'smtp_timeout'),
"local_domain" => null, "local_domain" => null,
]); ]);
return 'smtp';
}
return null;
} }
function base_ip(): string function base_ip(): string
@@ -212,7 +226,7 @@ function isDev(): bool
return config('app.env') === 'local'; return config('app.env') === 'local';
} }
function is_cloud(): bool function isCloud(): bool
{ {
return !config('coolify.self_hosted'); return !config('coolify.self_hosted');
} }
@@ -231,9 +245,9 @@ function validate_cron_expression($expression_to_validate): bool
function send_internal_notification(string $message): void function send_internal_notification(string $message): void
{ {
try { try {
$baseUrl = base_url(false); $baseUrl = config('app.name');
$team = Team::find(0); $team = Team::find(0);
$team->notify(new GeneralNotification("👀 Internal notifications from {$baseUrl}: " . $message)); $team->notify(new GeneralNotification("👀 {$baseUrl}: " . $message));
} catch (\Throwable $th) { } catch (\Throwable $th) {
ray($th->getMessage()); ray($th->getMessage());
} }
@@ -241,17 +255,40 @@ function send_internal_notification(string $message): void
function send_user_an_email(MailMessage $mail, string $email): void function send_user_an_email(MailMessage $mail, string $email): void
{ {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
set_transanctional_email_settings($settings); $type = set_transanctional_email_settings($settings);
if (!$type) {
throw new Exception('No email settings found.');
}
Mail::send( Mail::send(
[], [],
[], [],
fn (Message $message) => $message fn (Message $message) => $message
->from(
data_get($settings, 'smtp_from_address'),
data_get($settings, 'smtp_from_name')
)
->to($email) ->to($email)
->subject($mail->subject) ->subject($mail->subject)
->html((string) $mail->render()) ->html((string) $mail->render())
); );
} }
function isEmailEnabled($notifiable)
{
return data_get($notifiable, 'smtp_enabled') || data_get($notifiable, 'resend_enabled') || data_get($notifiable, 'use_instance_email_settings');
}
function setNotificationChannels($notifiable, $event)
{
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event");
$isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event");
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
}
if ($isEmailEnabled) {
$channels[] = EmailChannel::class;
}
if ($isTelegramEnabled && $isSubscribedToTelegramEvent) {
$channels[] = TelegramChannel::class;
}
return $channels;
}

View File

@@ -47,6 +47,9 @@ function getEndDate()
function isSubscriptionActive() function isSubscriptionActive()
{ {
if (!isCloud()) {
return false;
}
$team = currentTeam(); $team = currentTeam();
if (!$team) { if (!$team) {
return false; return false;
@@ -56,21 +59,19 @@ function isSubscriptionActive()
if (!$subscription) { if (!$subscription) {
return false; return false;
} }
if (config('subscription.provider') === 'lemon') { if (isLemon()) {
return $subscription->lemon_status === 'active'; return $subscription->lemon_status === 'active';
} }
if (config('subscription.provider') === 'stripe') { // if (isPaddle()) {
// return $subscription->paddle_status === 'active';
// }
if (isStripe()) {
return $subscription->stripe_invoice_paid === true && $subscription->stripe_cancel_at_period_end === false; return $subscription->stripe_invoice_paid === true && $subscription->stripe_cancel_at_period_end === false;
} }
return false; return false;
// if (config('subscription.provider') === 'paddle') {
// return $subscription->paddle_status === 'active';
// }
} }
function isSubscriptionOnGracePeriod() function isSubscriptionOnGracePeriod()
{ {
$team = currentTeam(); $team = currentTeam();
if (!$team) { if (!$team) {
return false; return false;
@@ -79,12 +80,12 @@ function isSubscriptionOnGracePeriod()
if (!$subscription) { if (!$subscription) {
return false; return false;
} }
if (config('subscription.provider') === 'lemon') { if (isLemon()) {
$is_still_grace_period = $subscription->lemon_ends_at && $is_still_grace_period = $subscription->lemon_ends_at &&
Carbon::parse($subscription->lemon_ends_at) > Carbon::now(); Carbon::parse($subscription->lemon_ends_at) > Carbon::now();
return $is_still_grace_period; return $is_still_grace_period;
} }
if (config('subscription.provider') === 'stripe') { if (isStripe()) {
return $subscription->stripe_cancel_at_period_end; return $subscription->stripe_cancel_at_period_end;
} }
return false; return false;
@@ -93,10 +94,22 @@ function subscriptionProvider()
{ {
return config('subscription.provider'); return config('subscription.provider');
} }
function isLemon()
{
return config('subscription.provider') === 'lemon';
}
function isStripe()
{
return config('subscription.provider') === 'stripe';
}
function isPaddle()
{
return config('subscription.provider') === 'paddle';
}
function getStripeCustomerPortalSession(Team $team) function getStripeCustomerPortalSession(Team $team)
{ {
Stripe::setApiKey(config('subscription.stripe_api_key')); Stripe::setApiKey(config('subscription.stripe_api_key'));
$return_url = route('team.show'); $return_url = route('team.index');
$stripe_customer_id = $team->subscription->stripe_customer_id; $stripe_customer_id = $team->subscription->stripe_customer_id;
$session = \Stripe\BillingPortal\Session::create([ $session = \Stripe\BillingPortal\Session::create([
'customer' => $stripe_customer_id, 'customer' => $stripe_customer_id,
@@ -124,6 +137,6 @@ function allowedPathsForBoardingAccounts()
return [ return [
...allowedPathsForUnsubscribedAccounts(), ...allowedPathsForUnsubscribedAccounts(),
'boarding', 'boarding',
'livewire/message/boarding', 'livewire/message/boarding.index',
]; ];
} }

View File

@@ -15,6 +15,7 @@
"laravel/fortify": "^v1.16.0", "laravel/fortify": "^v1.16.0",
"laravel/framework": "^v10.7.1", "laravel/framework": "^v10.7.1",
"laravel/horizon": "^5.15", "laravel/horizon": "^5.15",
"laravel/prompts": "^0.1.6",
"laravel/sanctum": "^v3.2.1", "laravel/sanctum": "^v3.2.1",
"laravel/tinker": "^v2.8.1", "laravel/tinker": "^v2.8.1",
"laravel/ui": "^4.2", "laravel/ui": "^4.2",
@@ -42,7 +43,7 @@
"laravel/pint": "^v1.8.0", "laravel/pint": "^v1.8.0",
"mockery/mockery": "^1.5.1", "mockery/mockery": "^1.5.1",
"nunomaduro/collision": "^v7.4.0", "nunomaduro/collision": "^v7.4.0",
"pestphp/pest": "^v2.4.0", "pestphp/pest": "^2.16",
"phpstan/phpstan": "^1.10", "phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.0.19", "phpunit/phpunit": "^10.0.19",
"serversideup/spin": "^v1.1.0", "serversideup/spin": "^v1.1.0",

142
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "da14dce99d76abcaaa6393166eda049a", "content-hash": "0603276b60e77cd859fabacdaaf31550",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@@ -6654,16 +6654,16 @@
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v6.3.2", "version": "v6.3.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "aa5d64ad3f63f2e48964fc81ee45cb318a723898" "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/aa5d64ad3f63f2e48964fc81ee45cb318a723898", "url": "https://api.github.com/repos/symfony/console/zipball/eca495f2ee845130855ddf1cf18460c38966c8b6",
"reference": "aa5d64ad3f63f2e48964fc81ee45cb318a723898", "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -6724,7 +6724,7 @@
"terminal" "terminal"
], ],
"support": { "support": {
"source": "https://github.com/symfony/console/tree/v6.3.2" "source": "https://github.com/symfony/console/tree/v6.3.4"
}, },
"funding": [ "funding": [
{ {
@@ -6740,7 +6740,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-07-19T20:17:28+00:00" "time": "2023-08-16T10:10:12+00:00"
}, },
{ {
"name": "symfony/css-selector", "name": "symfony/css-selector",
@@ -7761,16 +7761,16 @@
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.27.0", "version": "v1.28.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git", "url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a" "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a", "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -7785,7 +7785,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.27-dev" "dev-main": "1.28-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@@ -7823,7 +7823,7 @@
"portable" "portable"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
}, },
"funding": [ "funding": [
{ {
@@ -7839,7 +7839,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2023-01-26T09:26:14+00:00"
}, },
{ {
"name": "symfony/polyfill-iconv", "name": "symfony/polyfill-iconv",
@@ -7926,16 +7926,16 @@
}, },
{ {
"name": "symfony/polyfill-intl-grapheme", "name": "symfony/polyfill-intl-grapheme",
"version": "v1.27.0", "version": "v1.28.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "511a08c03c1960e08a883f4cffcacd219b758354" "reference": "875e90aeea2777b6f135677f618529449334a612"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612",
"reference": "511a08c03c1960e08a883f4cffcacd219b758354", "reference": "875e90aeea2777b6f135677f618529449334a612",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -7947,7 +7947,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.27-dev" "dev-main": "1.28-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@@ -7987,7 +7987,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0"
}, },
"funding": [ "funding": [
{ {
@@ -8003,7 +8003,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2023-01-26T09:26:14+00:00"
}, },
{ {
"name": "symfony/polyfill-intl-idn", "name": "symfony/polyfill-intl-idn",
@@ -8094,16 +8094,16 @@
}, },
{ {
"name": "symfony/polyfill-intl-normalizer", "name": "symfony/polyfill-intl-normalizer",
"version": "v1.27.0", "version": "v1.28.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
"reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -8115,7 +8115,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.27-dev" "dev-main": "1.28-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@@ -8158,7 +8158,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0"
}, },
"funding": [ "funding": [
{ {
@@ -8174,20 +8174,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2023-01-26T09:26:14+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.27.0", "version": "v1.28.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" "reference": "42292d99c55abe617799667f454222c54c60e229"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", "reference": "42292d99c55abe617799667f454222c54c60e229",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -8202,7 +8202,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.27-dev" "dev-main": "1.28-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@@ -8241,7 +8241,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
}, },
"funding": [ "funding": [
{ {
@@ -8257,7 +8257,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2023-07-28T09:04:16+00:00"
}, },
{ {
"name": "symfony/polyfill-php72", "name": "symfony/polyfill-php72",
@@ -8337,16 +8337,16 @@
}, },
{ {
"name": "symfony/polyfill-php80", "name": "symfony/polyfill-php80",
"version": "v1.27.0", "version": "v1.28.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-php80.git", "url": "https://github.com/symfony/polyfill-php80.git",
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -8355,7 +8355,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.27-dev" "dev-main": "1.28-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@@ -8400,7 +8400,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
}, },
"funding": [ "funding": [
{ {
@@ -8416,7 +8416,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-11-03T14:55:06+00:00" "time": "2023-01-26T09:26:14+00:00"
}, },
{ {
"name": "symfony/polyfill-php83", "name": "symfony/polyfill-php83",
@@ -8579,16 +8579,16 @@
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v6.3.2", "version": "v6.3.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/process.git", "url": "https://github.com/symfony/process.git",
"reference": "c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d" "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d", "url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54",
"reference": "c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d", "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -8620,7 +8620,7 @@
"description": "Executes commands in sub-processes", "description": "Executes commands in sub-processes",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/process/tree/v6.3.2" "source": "https://github.com/symfony/process/tree/v6.3.4"
}, },
"funding": [ "funding": [
{ {
@@ -8636,7 +8636,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-07-12T16:00:22+00:00" "time": "2023-08-07T10:39:22+00:00"
}, },
{ {
"name": "symfony/psr-http-message-bridge", "name": "symfony/psr-http-message-bridge",
@@ -9982,16 +9982,16 @@
"packages-dev": [ "packages-dev": [
{ {
"name": "brianium/paratest", "name": "brianium/paratest",
"version": "v7.2.5", "version": "v7.2.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/paratestphp/paratest.git", "url": "https://github.com/paratestphp/paratest.git",
"reference": "4d7ad5b6564f63baa1b948ecad05439f22880942" "reference": "7f372b5bb59b4271adedc67d3129df29b84c4173"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/4d7ad5b6564f63baa1b948ecad05439f22880942", "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7f372b5bb59b4271adedc67d3129df29b84c4173",
"reference": "4d7ad5b6564f63baa1b948ecad05439f22880942", "reference": "7f372b5bb59b4271adedc67d3129df29b84c4173",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10005,19 +10005,19 @@
"phpunit/php-code-coverage": "^10.1.3", "phpunit/php-code-coverage": "^10.1.3",
"phpunit/php-file-iterator": "^4.0.2", "phpunit/php-file-iterator": "^4.0.2",
"phpunit/php-timer": "^6.0", "phpunit/php-timer": "^6.0",
"phpunit/phpunit": "^10.3.1", "phpunit/phpunit": "^10.3.2",
"sebastian/environment": "^6.0.1", "sebastian/environment": "^6.0.1",
"symfony/console": "^6.3.2", "symfony/console": "^6.3.4",
"symfony/process": "^6.3.2" "symfony/process": "^6.3.4"
}, },
"require-dev": { "require-dev": {
"doctrine/coding-standard": "^12.0.0", "doctrine/coding-standard": "^12.0.0",
"ext-pcov": "*", "ext-pcov": "*",
"ext-posix": "*", "ext-posix": "*",
"infection/infection": "^0.27.0", "infection/infection": "^0.27.0",
"phpstan/phpstan": "^1.10.26", "phpstan/phpstan": "^1.10.32",
"phpstan/phpstan-deprecation-rules": "^1.1.3", "phpstan/phpstan-deprecation-rules": "^1.1.4",
"phpstan/phpstan-phpunit": "^1.3.13", "phpstan/phpstan-phpunit": "^1.3.14",
"phpstan/phpstan-strict-rules": "^1.5.1", "phpstan/phpstan-strict-rules": "^1.5.1",
"squizlabs/php_codesniffer": "^3.7.2", "squizlabs/php_codesniffer": "^3.7.2",
"symfony/filesystem": "^6.3.1" "symfony/filesystem": "^6.3.1"
@@ -10061,7 +10061,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/paratestphp/paratest/issues", "issues": "https://github.com/paratestphp/paratest/issues",
"source": "https://github.com/paratestphp/paratest/tree/v7.2.5" "source": "https://github.com/paratestphp/paratest/tree/v7.2.6"
}, },
"funding": [ "funding": [
{ {
@@ -10073,7 +10073,7 @@
"type": "paypal" "type": "paypal"
} }
], ],
"time": "2023-08-08T13:23:59+00:00" "time": "2023-08-29T07:47:39+00:00"
}, },
{ {
"name": "fakerphp/faker", "name": "fakerphp/faker",
@@ -10707,24 +10707,24 @@
}, },
{ {
"name": "pestphp/pest", "name": "pestphp/pest",
"version": "v2.16.0", "version": "v2.16.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/pestphp/pest.git", "url": "https://github.com/pestphp/pest.git",
"reference": "cbd6a650576714c673dbb0575989663f7f5c8b6d" "reference": "55b92666482b7d4320b7869c4eea7333d35c5631"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/pestphp/pest/zipball/cbd6a650576714c673dbb0575989663f7f5c8b6d", "url": "https://api.github.com/repos/pestphp/pest/zipball/55b92666482b7d4320b7869c4eea7333d35c5631",
"reference": "cbd6a650576714c673dbb0575989663f7f5c8b6d", "reference": "55b92666482b7d4320b7869c4eea7333d35c5631",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"brianium/paratest": "^7.2.5", "brianium/paratest": "^7.2.6",
"nunomaduro/collision": "^7.8.1", "nunomaduro/collision": "^7.8.1",
"nunomaduro/termwind": "^1.15.1", "nunomaduro/termwind": "^1.15.1",
"pestphp/pest-plugin": "^2.0.1", "pestphp/pest-plugin": "^2.1.1",
"pestphp/pest-plugin-arch": "^2.3.1", "pestphp/pest-plugin-arch": "^2.3.3",
"php": "^8.1.0", "php": "^8.1.0",
"phpunit/phpunit": "^10.3.2" "phpunit/phpunit": "^10.3.2"
}, },
@@ -10734,8 +10734,8 @@
}, },
"require-dev": { "require-dev": {
"pestphp/pest-dev-tools": "^2.16.0", "pestphp/pest-dev-tools": "^2.16.0",
"pestphp/pest-plugin-type-coverage": "^2.0.0", "pestphp/pest-plugin-type-coverage": "^2.2.0",
"symfony/process": "^6.3.2" "symfony/process": "^6.3.4"
}, },
"bin": [ "bin": [
"bin/pest" "bin/pest"
@@ -10793,7 +10793,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/pestphp/pest/issues", "issues": "https://github.com/pestphp/pest/issues",
"source": "https://github.com/pestphp/pest/tree/v2.16.0" "source": "https://github.com/pestphp/pest/tree/v2.16.1"
}, },
"funding": [ "funding": [
{ {
@@ -10805,7 +10805,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2023-08-21T08:42:07+00:00" "time": "2023-08-29T09:30:36+00:00"
}, },
{ {
"name": "pestphp/pest-plugin", "name": "pestphp/pest-plugin",

View File

@@ -17,7 +17,7 @@ return [
| |
*/ */
'name' => env('APP_NAME', 'Laravel'), 'name' => env('APP_NAME', 'Coolify'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@@ -94,7 +94,7 @@ return [
'users' => [ 'users' => [
'provider' => 'users', 'provider' => 'users',
'table' => 'password_reset_tokens', 'table' => 'password_reset_tokens',
'expire' => 60, 'expire' => 10,
'throttle' => 60, 'throttle' => 60,
], ],
], ],

View File

@@ -1,7 +1,7 @@
<?php <?php
return [ return [
'waitlist' => [ 'waitlist' => [
'confirmation_valid_for_minutes' => 10, 'expiration' => 10,
], ],
'invitation' => [ 'invitation' => [
'link' => [ 'link' => [
@@ -11,9 +11,18 @@ return [
], ],
'limits' => [ 'limits' => [
'server' => [ 'server' => [
'zero' => 0,
'self-hosted' => 999999999999,
'basic' => 1, 'basic' => 1,
'pro' => 3, 'pro' => 10,
'ultimate' => 9999999999999999999, 'ultimate' => 25,
],
'email' => [
'zero' => false,
'self-hosted' => true,
'basic' => false,
'pro' => true,
'ultimate' => true,
], ],
], ],
]; ];

View File

@@ -3,8 +3,9 @@
return [ return [
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),
'waitlist' => env('WAITLIST', false), 'waitlist' => env('WAITLIST', false),
'license_url' => 'https://license.coolify.io', 'license_url' => 'https://licenses.coollabs.io',
'mux_enabled' => env('MUX_ENABLED', true), 'mux_enabled' => env('MUX_ENABLED', true),
'dev_webhook' => env('SERVEO_URL'), 'dev_webhook' => env('SERVEO_URL'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'),
]; ];

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => trim(exec('jq -r .coolify.v4.version versions.json 2>/dev/null')) ?? 'unknown', 'release' => '4.0.0-beta.24',
'server_name' => env('APP_ID', 'coolify'), 'server_name' => env('APP_ID', 'coolify'),
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -18,7 +18,7 @@ return [
| |
*/ */
'driver' => env('SESSION_DRIVER', 'database'), 'driver' => env('SESSION_DRIVER', 'redis'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

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

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->string('stripe_plan_id')->nullable()->after('stripe_cancel_at_period_end');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->dropColumn('stripe_plan_id');
});
}
};

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