Compare commits

...

56 Commits

Author SHA1 Message Date
Andras Bacsai
9d826d9fb4 Merge pull request #1202 from coollabsio/next
v4.0.0-beta.26
2023-09-08 09:08:19 +02:00
Andras Bacsai
0af221f9fc udpate 2023-09-08 09:06:00 +02:00
Andras Bacsai
9066c9bf90 update readme 2023-09-08 09:00:44 +02:00
Andras Bacsai
77e3208f00 feat: public database 2023-09-07 13:23:34 +02:00
Andras Bacsai
db5ecf07bd version++ 2023-09-07 09:14:27 +02:00
Andras Bacsai
7f28aa6985 Merge pull request #1201 from coollabsio/next
v4.0.0-beta.25
2023-09-07 09:04:33 +02:00
Andras Bacsai
9d53e04ce9 fix: saveModel email settings 2023-09-07 08:48:16 +02:00
Andras Bacsai
3db9a1dd6e version++ 2023-09-06 15:26:17 +02:00
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
134 changed files with 1800 additions and 1217 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 }}

1
.gitignore vendored
View File

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

View File

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

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

@@ -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...",

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()->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTenMinutes()->onOneServer();
$schedule->job(new ResourceStatusJob)->everyMinute()->onOneServer(); // $schedule->job(new ResourceStatusJob)->everyMinute()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
$schedule->job(new ProxyCheckJob)->everyFiveMinutes()->onOneServer(); $schedule->job(new ProxyCheckJob)->everyFiveMinutes()->onOneServer();
$schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer(); $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

@@ -8,15 +8,41 @@ use App\Models\S3Storage;
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 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 link()
{
$token = request()->get('token');
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 (!isCloud()) { if (!isCloud()) {
@@ -37,10 +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 boarding() { public function boarding()
{
if (currentTeam()->boarding || isDev()) { if (currentTeam()->boarding || isDev()) {
return view('boarding'); return view('boarding');
} else { } else {

View File

@@ -60,9 +60,6 @@ class ProjectController extends Controller
'environment_name' => $environment->name, 'environment_name' => $environment->name,
'database_uuid' => $standalone_postgresql->uuid, 'database_uuid' => $standalone_postgresql->uuid,
]); ]);
}
if ($server) {
} }
return view('project.new', [ return view('project.new', [
'type' => $type 'type' => $type

View File

@@ -25,6 +25,15 @@ class Dashboard extends Component
} }
$this->projects = $projects->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() public function render()
{ {
return view('livewire.dashboard'); return view('livewire.dashboard');

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

@@ -49,6 +49,7 @@ class EmailSettings extends Component
public function mount() public function mount()
{ {
$this->team = auth()->user()->currentTeam();
['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits; ['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits;
$this->emails = auth()->user()->email; $this->emails = auth()->user()->email;
} }
@@ -106,7 +107,14 @@ class EmailSettings extends Component
return general_error_handler($e, $this); return general_error_handler($e, $this);
} }
} }
public function saveModel()
{
$this->team->save();
if (is_a($this->team, Team::class)) {
refreshSession();
}
$this->emit('success', 'Settings saved.');
}
public function submit() public function submit()
{ {
try { try {

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

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

View File

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

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

@@ -22,34 +22,52 @@ class Select extends Component
public Collection|array $swarmDockers = []; public Collection|array $swarmDockers = [];
public array $parameters; public array $parameters;
public ?string $existingPostgresqlUrl = null;
protected $queryString = [ protected $queryString = [
'server', '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)) { if (!is_null($this->server)) {
$foundServer = $this->servers->where('id', $this->server)->first(); $foundServer = $this->servers->where('id', $this->server)->first();
if ($foundServer) { if ($foundServer) {
return $this->set_server($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->standaloneDockers = $server->standaloneDockers; $this->standaloneDockers = $server->standaloneDockers;
@@ -57,7 +75,7 @@ class Select extends Component
$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

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

@@ -32,16 +32,19 @@ 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')) { if (session('from')) {
session(['from' => session('from') + ['source_id' => $github_app->id]]); session(['from' => session('from') + ['source_id' => $github_app->id]]);
} }

View File

@@ -47,6 +47,9 @@ 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('dashboard', ['success' => true]), 'success_url' => route('dashboard', ['success' => true]),
'cancel_url' => route('subscription.index', ['cancelled' => true]), 'cancel_url' => route('subscription.index', ['cancelled' => true]),

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

@@ -66,12 +66,7 @@ class ApplicationDeploymentJob implements ShouldQueue
private $log_model; private $log_model;
private Collection $saved_outputs; private Collection $saved_outputs;
public function middleware(): array public $tries = 1;
{
return [
(new WithoutOverlapping("dockerimagejobs"))->shared(),
];
}
public function __construct(int $application_deployment_queue_id) public function __construct(int $application_deployment_queue_id)
{ {
ray()->clearScreen(); ray()->clearScreen();
@@ -181,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();
@@ -206,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) {
@@ -242,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(
@@ -264,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')) {
@@ -282,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}.'",
@@ -304,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,
], ],
[ [
@@ -494,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,
] ]
@@ -654,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}
@@ -692,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
]); ]);
} }
} }
@@ -706,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

@@ -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,6 +2,7 @@
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;
@@ -9,7 +10,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping; 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
{ {
@@ -30,6 +30,11 @@ class DockerCleanupJob implements ShouldQueue
} }
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'); ray()->showQueries()->color('orange');
$servers = Server::all(); $servers = Server::all();

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

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

@@ -24,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);

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 = isEmailEnabled($notifiable);
$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

@@ -9,7 +9,6 @@ use Illuminate\Support\Facades\Mail;
class EmailChannel class EmailChannel
{ {
private bool $isResend = false;
public function send(SendsEmail $notifiable, Notification $notification): void public function send(SendsEmail $notifiable, Notification $notification): void
{ {
$this->bootConfigs($notifiable); $this->bootConfigs($notifiable);
@@ -20,35 +19,14 @@ class EmailChannel
} }
$mailMessage = $notification->toMail($notifiable); $mailMessage = $notification->toMail($notifiable);
if ($this->isResend) { Mail::send(
foreach ($recepients as $receipient) { [],
Mail::send( [],
[], fn (Message $message) => $message
[], ->to($recepients)
fn (Message $message) => $message ->subject($mailMessage->subject)
->from( ->html((string)$mailMessage->render())
data_get($notifiable, 'smtp_from_address'), );
data_get($notifiable, 'smtp_from_name'),
)
->to($receipient)
->subject($mailMessage->subject)
->html((string)$mailMessage->render())
);
}
} else {
Mail::send(
[],
[],
fn (Message $message) => $message
->from(
data_get($notifiable, 'smtp_from_address'),
data_get($notifiable, 'smtp_from_name'),
)
->bcc($recepients)
->subject($mailMessage->subject)
->html((string)$mailMessage->render())
);
}
} }
private function bootConfigs($notifiable): void private function bootConfigs($notifiable): void
@@ -58,13 +36,11 @@ class EmailChannel
if (!$type) { if (!$type) {
throw new Exception('No email settings found.'); throw new Exception('No email settings found.');
} }
if ($type === 'resend') {
$this->isResend = true;
}
return; 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')) { if (data_get($notifiable, 'resend_enabled')) {
$this->isResend = true;
config()->set('mail.default', 'resend'); config()->set('mail.default', 'resend');
config()->set('resend.api_key', data_get($notifiable, 'resend_api_key')); config()->set('resend.api_key', data_get($notifiable, 'resend_api_key'));
} }

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

@@ -12,7 +12,6 @@ use Log;
class TransactionalEmailChannel class TransactionalEmailChannel
{ {
private bool $isResend = false;
public function send(User $notifiable, Notification $notification): void public function send(User $notifiable, Notification $notification): void
{ {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
@@ -26,33 +25,14 @@ class TransactionalEmailChannel
} }
$this->bootConfigs(); $this->bootConfigs();
$mailMessage = $notification->toMail($notifiable); $mailMessage = $notification->toMail($notifiable);
if ($this->isResend) { Mail::send(
Mail::send( [],
[], [],
[], fn (Message $message) => $message
fn (Message $message) => $message ->to($email)
->from( ->subject($mailMessage->subject)
data_get($settings, 'smtp_from_address'), ->html((string)$mailMessage->render())
data_get($settings, 'smtp_from_name'), );
)
->to($email)
->subject($mailMessage->subject)
->html((string)$mailMessage->render())
);
} else {
Mail::send(
[],
[],
fn (Message $message) => $message
->from(
data_get($settings, 'smtp_from_address'),
data_get($settings, 'smtp_from_name'),
)
->bcc($email)
->subject($mailMessage->subject)
->html((string)$mailMessage->render())
);
}
} }
private function bootConfigs(): void private function bootConfigs(): void
@@ -61,8 +41,5 @@ class TransactionalEmailChannel
if (!$type) { if (!$type) {
throw new Exception('No email settings found.'); throw new Exception('No email settings found.');
} }
if ($type === 'resend') {
$this->isResend = true;
}
} }
} }

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 = isEmailEnabled($notifiable);
$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 = isEmailEnabled($notifiable);
$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 = isEmailEnabled($notifiable);
$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 = isEmailEnabled($notifiable);
$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" => base_url()
]
],
];
}
} }

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

@@ -50,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

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

View File

@@ -86,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

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

View File

@@ -77,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 '
@@ -92,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);
@@ -216,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,12 +2,14 @@
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;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@@ -26,6 +28,10 @@ function database_configuration_dir(): string
{ {
return '/data/coolify/databases'; return '/data/coolify/databases';
} }
function database_proxy_dir($uuid): string
{
return "/data/coolify/databases/$uuid/proxy";
}
function backup_dir(): string function backup_dir(): string
{ {
@@ -149,6 +155,8 @@ function set_transanctional_email_settings(InstanceSettings | null $settings = n
if (!$settings) { if (!$settings) {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
} }
config()->set('mail.from.address', data_get($settings, 'smtp_from_address'));
config()->set('mail.from.name', data_get($settings, 'smtp_from_name'));
if (data_get($settings, 'resend_enabled')) { if (data_get($settings, 'resend_enabled')) {
config()->set('mail.default', 'resend'); config()->set('mail.default', 'resend');
config()->set('resend.api_key', data_get($settings, 'resend_api_key')); config()->set('resend.api_key', data_get($settings, 'resend_api_key'));
@@ -241,9 +249,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());
} }
@@ -259,17 +267,32 @@ function send_user_an_email(MailMessage $mail, string $email): void
[], [],
[], [],
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) function isEmailEnabled($notifiable)
{ {
return data_get($notifiable, 'smtp_enabled') || data_get($notifiable, 'resend_enabled') || data_get($notifiable, 'use_instance_email_settings'); 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;

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",

2
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": "dbb08df7a80c46ce2b9b9fa397ed71c1", "content-hash": "0603276b60e77cd859fabacdaaf31550",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",

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

@@ -7,4 +7,5 @@ return [
'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' => '4.0.0-beta.22', 'release' => '4.0.0-beta.26',
'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

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.22'; return '4.0.0-beta.26';

View File

@@ -0,0 +1,32 @@
<?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_feedback')->nullable()->after('stripe_cancel_at_period_end');
$table->string('stripe_comment')->nullable()->after('stripe_feedback');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->dropColumn('stripe_feedback');
$table->dropColumn('stripe_comment');
});
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->boolean('telegram_enabled')->default(false);
$table->text('telegram_token')->nullable();
$table->text('telegram_chat_id')->nullable();
$table->boolean('telegram_notifications_test')->default(true);
$table->boolean('telegram_notifications_deployments')->default(true);
$table->boolean('telegram_notifications_status_changes')->default(true);
$table->boolean('telegram_notifications_database_backups')->default(true);
});
}
public function down(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->dropColumn('telegram_enabled');
$table->dropColumn('telegram_token');
$table->dropColumn('telegram_chat_id');
$table->dropColumn('telegram_notifications_test');
$table->dropColumn('telegram_notifications_deployments');
$table->dropColumn('telegram_notifications_status_changes');
$table->dropColumn('telegram_notifications_database_backups');
});
}
};

View File

@@ -45,7 +45,7 @@ class ProductionSeeder extends Seeder
]); ]);
} }
if (config('app.name') !== 'coolify-cloud') { if (config('app.name') !== 'Coolify Cloud') {
// Save SSH Keys for the Coolify Host // Save SSH Keys for the Coolify Host
$coolify_key_name = "id.root@host.docker.internal"; $coolify_key_name = "id.root@host.docker.internal";
$coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}"); $coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}");

View File

@@ -6,6 +6,7 @@ x-testing-host: &testing-host-base
context: ./docker/testing-host context: ./docker/testing-host
networks: networks:
- coolify - coolify
init: true
services: services:
coolify: coolify:
@@ -53,6 +54,7 @@ services:
<<: *testing-host-base <<: *testing-host-base
container_name: coolify-testing-host container_name: coolify-testing-host
volumes: volumes:
- /:/host
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- /data/coolify/:/data/coolify - /data/coolify/:/data/coolify
mailpit: mailpit:

View File

@@ -2,15 +2,15 @@ FROM alpine:3.17
ARG TARGETPLATFORM ARG TARGETPLATFORM
# https://download.docker.com/linux/static/stable/ # https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=23.0.6 ARG DOCKER_VERSION=24.0.5
# https://github.com/docker/compose/releases # https://github.com/docker/compose/releases
ARG DOCKER_COMPOSE_VERSION=2.18.1 ARG DOCKER_COMPOSE_VERSION=2.21.0
# https://github.com/docker/buildx/releases # https://github.com/docker/buildx/releases
ARG DOCKER_BUILDX_VERSION=0.10.5 ARG DOCKER_BUILDX_VERSION=0.11.2
# https://github.com/buildpacks/pack/releases # https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.29.0 ARG PACK_VERSION=0.30.0
# https://github.com/railwayapp/nixpacks/releases # https://github.com/railwayapp/nixpacks/releases
ARG NIXPACKS_VERSION=1.12.0 ARG NIXPACKS_VERSION=1.13.0
USER root USER root
WORKDIR /artifacts WORKDIR /artifacts
@@ -38,5 +38,5 @@ COPY --from=minio/mc /usr/bin/mc /usr/bin/mc
RUN chmod +x /usr/bin/mc RUN chmod +x /usr/bin/mc
ENTRYPOINT ["/sbin/tini", "--"] ENTRYPOINT ["/sbin/tini", "--"]
CMD ["sh", "-c", "while true; do sleep 1; done"] CMD ["tail", "-f", "/dev/null"]

View File

@@ -1,29 +1,28 @@
FROM alpine:3.17 FROM debian:12-slim
ARG TARGETPLATFORM ARG TARGETPLATFORM
# https://download.docker.com/linux/static/stable/ # https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=23.0.6 ARG DOCKER_VERSION=24.0.5
# https://github.com/docker/compose/releases # https://github.com/docker/compose/releases
ARG DOCKER_COMPOSE_VERSION=2.18.1 ARG DOCKER_COMPOSE_VERSION=2.21.0
# https://github.com/docker/buildx/releases # https://github.com/docker/buildx/releases
ARG DOCKER_BUILDX_VERSION=0.10.5 ARG DOCKER_BUILDX_VERSION=0.11.2
# https://github.com/buildpacks/pack/releases # https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.29.0 ARG PACK_VERSION=0.30.0
# https://github.com/railwayapp/nixpacks/releases # https://github.com/railwayapp/nixpacks/releases
ARG NIXPACKS_VERSION=1.12.0 ARG NIXPACKS_VERSION=1.13.0
USER root USER root
WORKDIR /root WORKDIR /root
RUN apk add --no-cache bash curl git git-lfs openssh-client openssh-server tar tini postgresql-client lsof ENV PATH "$PATH:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin"
RUN apt update && apt -y install openssh-client openssh-server curl wget git jq jc
RUN mkdir -p ~/.docker/cli-plugins RUN mkdir -p ~/.docker/cli-plugins
RUN if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \ RUN curl -sSL https://github.com/docker/buildx/releases/download/v${DOCKER_BUILDX_VERSION}/buildx-v${DOCKER_BUILDX_VERSION}.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
curl -sSL https://github.com/docker/buildx/releases/download/v${DOCKER_BUILDX_VERSION}/buildx-v${DOCKER_BUILDX_VERSION}.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx && \ RUN curl -sSL https://github.com/docker/compose/releases/download/v${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose
curl -sSL https://github.com/docker/compose/releases/download/v${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose && \ RUN (curl -sSL https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz | tar -C /usr/bin/ --no-same-owner -xzv --strip-components=1 docker/docker)
(curl -sSL https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz | tar -C /usr/bin/ --no-same-owner -xzv --strip-components=1 docker/docker) && \ RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /root/.docker/cli-plugins/docker-buildx
(curl -sSL https://github.com/buildpacks/pack/releases/download/v${PACK_VERSION}/pack-v${PACK_VERSION}-linux.tgz | tar -C /usr/local/bin/ --no-same-owner -xzv pack) && \
curl -sSL https://nixpacks.com/install.sh | bash && \
chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack /root/.docker/cli-plugins/docker-buildx \
;fi
# Setup sshd # Setup sshd
RUN ssh-keygen -A RUN ssh-keygen -A
@@ -32,6 +31,4 @@ RUN mkdir -p ~/.ssh
RUN echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68 coolify@coolify-instance" >> ~/.ssh/authorized_keys RUN echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68 coolify@coolify-instance" >> ~/.ssh/authorized_keys
EXPOSE 22 EXPOSE 22
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/usr/sbin/sshd", "-D", "-o", "ListenAddress=0.0.0.0"] CMD ["/usr/sbin/sshd", "-D", "-o", "ListenAddress=0.0.0.0"]

View File

@@ -593,7 +593,7 @@ async function redirect() {
targetUrl.pathname = `/source/new` targetUrl.pathname = `/source/new`
break; break;
case 7: case 7:
targetUrl.pathname = `/private-key/new` targetUrl.pathname = `/security/private-key/new`
break; break;
case 8: case 8:
targetUrl.pathname = `/destination/new` targetUrl.pathname = `/destination/new`
@@ -612,7 +612,7 @@ async function redirect() {
targetUrl.pathname = `/servers` targetUrl.pathname = `/servers`
break; break;
case 13: case 13:
targetUrl.pathname = `/private-keys` targetUrl.pathname = `/security/private-key`
break; break;
case 14: case 14:
targetUrl.pathname = `/projects` targetUrl.pathname = `/projects`

View File

@@ -51,6 +51,11 @@
{{ session('status') }} {{ session('status') }}
</div> </div>
@endif @endif
@if (session('error'))
<div class="mb-4 font-medium text-red-600">
{{ session('error') }}
</div>
@endif
</form> </form>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,6 @@
{{ Illuminate\Mail\Markdown::parse('---') }}
Thank you,<br>
{{ config('app.name') ?? 'Coolify' }}
{{ Illuminate\Mail\Markdown::parse('[Contact Support](https://docs.coollabs.io)') }}

View File

@@ -0,0 +1 @@
Hello,

View File

@@ -0,0 +1,6 @@
<x-emails.header />
{{ Illuminate\Mail\Markdown::parse($slot) }}
<x-emails.footer />

View File

@@ -12,8 +12,8 @@
@if ($attributes->get('type') === 'submit') @if ($attributes->get('type') === 'submit')
<span wire:target="submit" wire:loading.delay class="loading loading-xs text-warning loading-spinner"></span> <span wire:target="submit" wire:loading.delay class="loading loading-xs text-warning loading-spinner"></span>
@else @else
@if ($attributes->has('wire:click')) @if ($attributes->whereStartsWith('wire:click')->first())
<span wire:target="{{ $attributes->get('wire:click') }}" wire:loading.delay <span wire:target="{{ $attributes->whereStartsWith('wire:click')->first() }}" wire:loading.delay
class="loading loading-xs loading-spinner"></span> class="loading loading-xs loading-spinner"></span>
@endif @endif
@endif @endif

View File

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

View File

@@ -26,7 +26,8 @@
wire:model.defer={{ $id }} wire:dirty.class.remove='text-white' wire:model.defer={{ $id }} wire:dirty.class.remove='text-white'
wire:dirty.class="input-warning" wire:loading.attr="disabled" type="{{ $type }}" wire:dirty.class="input-warning" wire:loading.attr="disabled" type="{{ $type }}"
@readonly($readonly) @disabled($disabled) id="{{ $id }}" name="{{ $name }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}" name="{{ $name }}"
placeholder="{{ $attributes->get('placeholder') }}"> placeholder="{{ $attributes->get('placeholder') }}"
aria-placeholder="{{ $attributes->get('placeholder') }}">
</div> </div>
@else @else

View File

@@ -5,8 +5,8 @@
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg> </svg>
</div> </div>
<div class="absolute hidden text-xs rounded group-hover:block border-coolgray-400 bg-coolgray-500"> <div class="absolute z-40 hidden text-xs rounded group-hover:block border-coolgray-400 bg-coolgray-500">
<div class="p-4 card-body"> <div class="p-4">
{!! $helper !!} {!! $helper !!}
</div> </div>
</div> </div>

View File

@@ -43,10 +43,6 @@
@endisset @endisset
@if ($modalSubmit) @if ($modalSubmit)
{{ $modalSubmit }} {{ $modalSubmit }}
@else
<x-forms.button onclick="{{ $modalId }}.close()" type="submit">
Save
</x-forms.button>
@endif @endif
</form> </form>

View File

@@ -50,6 +50,23 @@
</svg> </svg>
</a> </a>
</li> </li>
<li title="Source">
<a class="hover:bg-transparent" href="{{ route('source.all') }}">
<svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="m6.793 1.207l.353.354l-.353-.354ZM1.207 6.793l-.353-.354l.353.354Zm0 1.414l.354-.353l-.354.353Zm5.586 5.586l-.354.353l.354-.353Zm1.414 0l-.353-.354l.353.354Zm5.586-5.586l.353.354l-.353-.354Zm0-1.414l-.354.353l.354-.353ZM8.207 1.207l.354-.353l-.354.353ZM6.44.854L.854 6.439l.707.707l5.585-5.585L6.44.854ZM.854 8.56l5.585 5.585l.707-.707l-5.585-5.585l-.707.707Zm7.707 5.585l5.585-5.585l-.707-.707l-5.585 5.585l.707.707Zm5.585-7.707L8.561.854l-.707.707l5.585 5.585l.707-.707Zm0 2.122a1.5 1.5 0 0 0 0-2.122l-.707.707a.5.5 0 0 1 0 .708l.707.707ZM6.44 14.146a1.5 1.5 0 0 0 2.122 0l-.707-.707a.5.5 0 0 1-.708 0l-.707.707ZM.854 6.44a1.5 1.5 0 0 0 0 2.122l.707-.707a.5.5 0 0 1 0-.708L.854 6.44Zm6.292-4.878a.5.5 0 0 1 .708 0L8.56.854a1.5 1.5 0 0 0-2.122 0l.707.707Zm-2 1.293l1 1l.708-.708l-1-1l-.708.708ZM7.5 5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 6V5Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 4.5H8ZM7.5 4a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 3v1Zm0-1A1.5 1.5 0 0 0 6 4.5h1a.5.5 0 0 1 .5-.5V3Zm.646 2.854l1.5 1.5l.707-.708l-1.5-1.5l-.707.708ZM10.5 8a.5.5 0 0 1-.5-.5H9A1.5 1.5 0 0 0 10.5 9V8Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 12 7.5h-1Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 10.5 6v1Zm0-1A1.5 1.5 0 0 0 9 7.5h1a.5.5 0 0 1 .5-.5V6ZM7 5.5v4h1v-4H7Zm.5 5.5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 12v-1Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 10.5H8Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 9v1Zm0-1A1.5 1.5 0 0 0 6 10.5h1a.5.5 0 0 1 .5-.5V9Z" />
</svg>
</a>
</li>
<li title="Security">
<a class="hover:bg-transparent" href="{{ route('security.private-key.index') }}">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="m16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1-4.069 0l-.301-.301l-6.558 6.558a2 2 0 0 1-1.239.578L5.172 21H4a1 1 0 0 1-.993-.883L3 20v-1.172a2 2 0 0 1 .467-1.284l.119-.13L4 17h2v-2h2v-2l2.144-2.144l-.301-.301a2.877 2.877 0 0 1 0-4.069l2.643-2.643a2.877 2.877 0 0 1 4.069 0zM15 9h.01" />
</svg>
</a>
</li>
<li title="Teams"> <li title="Teams">
<a class="hover:bg-transparent" href="{{ route('team.index') }}"> <a class="hover:bg-transparent" href="{{ route('team.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
@@ -64,6 +81,7 @@
</svg> </svg>
</a> </a>
</li> </li>
<div class="flex-1"></div> <div class="flex-1"></div>
@if (isInstanceAdmin() && !isCloud()) @if (isInstanceAdmin() && !isCloud())
<livewire:upgrade /> <livewire:upgrade />
@@ -95,6 +113,20 @@
</a> </a>
</li> </li>
@endif @endif
@if (isSubscriptionActive() || isDev())
<li title="Help" class="mt-auto">
<div class="justify-center icons" wire:click="help" onclick="help.showModal()">
<svg class="{{ request()->is('help*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0-18 0m9 4v.01" />
<path d="M12 13a2 2 0 0 0 .914-3.782a1.98 1.98 0 0 0-2.414.483" />
</g>
</svg>
</div>
</li>
@endif
<li class="pb-6" title="Logout"> <li class="pb-6" title="Logout">
<form action="/logout" method="POST" class=" hover:bg-transparent"> <form action="/logout" method="POST" class=" hover:bg-transparent">
@csrf @csrf

View File

@@ -0,0 +1,17 @@
<div class="pb-6">
<h1>Security</h1>
<nav class="flex pt-2 pb-10">
<ol class="inline-flex items-center">
<li>
<div class="flex items-center">
<span>Security related settings</span>
</div>
</li>
</ol>
</nav>
<nav class="navbar-main">
<a class="{{ request()->routeIs('security.private-key.index') ? 'text-white' : '' }}" href="{{ route('security.private-key.index') }}">
<button>Private Keys</button>
</a>
</nav>
</div>

View File

@@ -1,8 +1,11 @@
@if ($pull_request_id !== 0) <x-emails.layout>
Pull Request #{{ $pull_request_id }} of {{ $name }} (<a target="_blank" @if ($pull_request_id === 0)
href="{{ $fqdn }}">{{ $fqdn }}</a>) deployment failed: Failed to deploy a new version of {{ $name }} at [{{ $fqdn }}]({{ $fqdn }}) .
@else @else
Deployment failed of {{ $name }} (<a target="_blank" href="{{ $fqdn }}">{{ $fqdn }}</a>): Failed to deploy a pull request #{{ $pull_request_id }} of {{ $name }} at
@endif [{{ $fqdn }}]({{ $fqdn }}).
@endif
<a target="_blank" href="{{ $deployment_url }}">View Deployment Logs</a><br><br> [View Deployment Logs]({{ $deployment_url }})
</x-emails.layout>

View File

@@ -1,8 +1,10 @@
@if ($pull_request_id === 0) <x-emails.layout>
A new version of <a target="_blank" href="{{ $fqdn }}">{{ $fqdn }}</a> is available: @if ($pull_request_id === 0)
@else A new version of {{ $name }} is available at [{{ $fqdn }}]({{ $fqdn }}) .
Pull request #{{ $pull_request_id }} of {{ $name }} deployed successfully: <a target="_blank" @else
href="{{ $fqdn }}">Application Link</a> | Pull request #{{ $pull_request_id }} of {{ $name }} deployed successfully [{{ $fqdn }}]({{ $fqdn }}).
@endif @endif
<a target="_blank" href="{{ $deployment_url }}">View
Deployment Logs</a><br><br> [View Deployment Logs]({{ $deployment_url }})
</x-emails.layout>

View File

@@ -1,2 +1,9 @@
{{ $name }} has been stopped.<br><br> <x-emails.layout>
<a target="_blank" href="{{ $application_url }}">Open in Coolify</a><br><br>
{{ $name }} has been stopped.
If it was your intention to stop this application, you can ignore this email.
If not, [check what is going on]({{ $application_url }}).
</x-emails.layout>

View File

@@ -0,0 +1,8 @@
<x-emails.layout>
Database backup for {{ $name }} with frequency of {{ $frequency }} was FAILED.
### Reason
{{ $output }}
</x-emails.layout>

View File

@@ -0,0 +1,3 @@
<x-emails.layout>
Database backup for {{ $name }} with frequency of {{ $frequency }} was successful.
</x-emails.layout>

View File

@@ -0,0 +1,5 @@
{{ $description }}
{{ Illuminate\Mail\Markdown::parse('---') }}
{{ Illuminate\Mail\Markdown::parse($debug) }}

View File

@@ -1,13 +1,11 @@
Hello,<br><br> <x-emails.layout>
You have been invited to "{{ $team }}" on "{{ config('app.name') }}".<br><br> You have been invited to "{{ $team }}" on "{{ config('app.name') }}".
Please click here to accept the invitation: <a target="_blank" href="{{ $invitation_link }}">Accept Invitation</a><br> Please [click here]({{ $invitation_link }}) to accept the invitation.
<br>
If you have any questions, please contact the team owner.<br><br> If you have any questions, please contact the team owner.<br><br>
If it was not you who requested this invitation, please ignore this ema il, or instantly revoke the invitation by If it was not you who requested this invitation, please ignore this email, or instantly revoke the invitation by clicking [here]({{ $invitation_link }}/revoke).
clicking here: <a target="_blank" href="{{ $invitation_link }}/revoke">Revoke Invitation</a><br><br>
Thank you. </x-emails.layout>

View File

@@ -1,6 +1,7 @@
A password reset requested for your email address on "{{ config('app.name') }}".<br><br> <x-emails.layout>
A password reset has been requested for this email address.
Please click the following link to reset your password: <a target="_blank" href="{{ $url }}">Password Click [here]({{ $url }}) to reset your password.
Reset</a><br><br>
This password reset link will expire in {{ $count }} minutes. This link will expire in {{ $count }} minutes.
</x-emails.layout>

View File

@@ -1,4 +1,6 @@
Your last invoice has failed to be paid for Coolify Cloud. Please <a href="{{$stripeCustomerPortal}}">update payment details on your Stripe Customer Portal</a>. <x-emails.layout>
<br><br> Your last invoice has failed to be paid for Coolify Cloud.
Thanks,<br>
Coolify Cloud Please update payment details [here]({{$stripeCustomerPortal}}).
</x-emails.layout>

View File

@@ -1 +1,3 @@
If you are seeing this, it means that your SMTP settings are correct. <x-emails.layout>
If you are seeing this, it means that your Email settings are correct.
</x-emails.layout>

View File

@@ -1,4 +1,9 @@
Someone added this email to the Coolify Cloud's waitlist. <x-emails.layout>
<br> Someone added this email to the Coolify Cloud's waitlist. [Click here]({{ $confirmation_url }}) to confirm!
<a href="{{ $confirmation_url }}">Click here to confirm</a>! The link will expire in {{config('constants.waitlist.expiration')}} minutes.<br><br>
You have no idea what <a href="https://coolify.io">Coolify Cloud</a> is or this waitlist? <a href="{{ $cancel_url }}">Click here to remove</a> you from the waitlist. The link will expire in {{config('constants.waitlist.expiration')}} minutes.
You have no idea what [Coolify Cloud](https://coolify.io) is or this waitlist? [Click here]({{ $cancel_url }}) to remove you from the waitlist.
</x-emails.layout>

View File

@@ -1,13 +1,4 @@
You have been invited to join the Coolify Cloud. <a href="{{base_url()}}/login">Login here</a> <x-emails.layout>
<br> You have been invited to join the Coolify Cloud: [Get Started]({{$loginLink}})
<br> </x-emails.layout>
Here is your initial login information.
<br>
Email: <br>
{{ $email }}
<br><br>
Password:<br>
{{ $password }}
<br><br>
(You will forced to change it on first login.)

View File

@@ -1,5 +1,20 @@
@extends('errors::minimal') @extends('layouts.base')
<div class="min-h-screen hero">
@section('title', __('Unauthorized')) <div class="text-center hero-content">
@section('code', '401') <div class="">
@section('message', __('Unauthorized')) <p class="font-mono text-6xl font-semibold text-warning">401</p>
<h1 class="mt-4 font-bold tracking-tight text-white">You shall not pass!</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">You don't have permission to access this page.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a target="_blank" class="text-xs" href="https://docs.coollabs.io/contact.html">Contact
support
<x-external-link />
</a>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,20 @@
@extends('errors::minimal') @extends('layouts.base')
<div class="min-h-screen hero">
@section('title', __('Forbidden')) <div class="text-center hero-content">
@section('code', '403') <div class="">
@section('message', __($exception->getMessage() ?: 'Forbidden')) <p class="font-mono text-6xl font-semibold text-warning">403</p>
<h1 class="mt-4 font-bold tracking-tight text-white">You shall not pass!</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">You don't have permission to access this page.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a target="_blank" class="text-xs" href="https://docs.coollabs.io/contact.html">Contact
support
<x-external-link />
</a>
</div>
</div>
</div>
</div>

View File

@@ -1,22 +1,21 @@
<x-layout> @extends('layouts.base')
<div class="min-h-screen hero"> <div class="min-h-screen hero">
<div class="text-center hero-content"> <div class="text-center hero-content">
<div class=""> <div class="">
<p class="font-mono text-6xl font-semibold text-warning">404</p> <p class="font-mono text-6xl font-semibold text-warning">404</p>
<h1 class="mt-4 font-bold tracking-tight text-white">How did you got here?</h1> <h1 class="mt-4 font-bold tracking-tight text-white">How did you got here?</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">Sorry, we couldnt find the page youre looking <p class="mt-6 text-base leading-7 text-neutral-300">Sorry, we couldnt find the page youre looking
for. for.
</p> </p>
<div class="flex items-center justify-center mt-10 gap-x-6"> <div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/"> <a href="/">
<x-forms.button isHighlighted>Go back home</x-forms.button> <x-forms.button>Go back home</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="https://docs.coollabs.io/contact.html">Contact <a target="_blank" class="text-xs" href="https://docs.coollabs.io/contact.html">Contact
support support
<x-external-link /> <x-external-link />
</a> </a>
</div>
</div> </div>
</div> </div>
</div> </div>
</x-layout> </div>

View File

@@ -1,21 +1,20 @@
<x-layout> @extends('layouts.base')
<div class="min-h-screen hero"> <div class="min-h-screen hero">
<div class="text-center hero-content"> <div class="text-center hero-content">
<div class=""> <div class="">
<p class="font-mono text-6xl font-semibold text-warning">419</p> <p class="font-mono text-6xl font-semibold text-warning">419</p>
<h1 class="mt-4 font-bold tracking-tight text-white">This page is definitely old</h1> <h1 class="mt-4 font-bold tracking-tight text-white">This page is definitely old, not like you!</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">Sorry, we couldnt find the page youre looking <p class="mt-6 text-base leading-7 text-neutral-300">Sorry, we couldnt find the page youre looking
for. for.
</p> </p>
<div class="flex items-center justify-center mt-10 gap-x-6"> <div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/" <a href="/">
class="rounded-md bg-coollabs px-3.5 py-2.5 font-semibold text-white shadow-sm hover:bg-coollabs-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 hover:no-underline">Go <x-forms.button>Go back home</x-forms.button>
back home</a> </a>
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact <a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact
support support
<span aria-hidden="true">&rarr;</span></a> <span aria-hidden="true">&rarr;</span></a>
</div>
</div> </div>
</div> </div>
</div> </div>
</x-layout> </div>

View File

@@ -1,5 +1,19 @@
@extends('errors::minimal') @extends('layouts.base')
<div class="min-h-screen hero">
@section('title', __('Too Many Requests')) <div class="text-center hero-content">
@section('code', '429') <div class="">
@section('message', __('Too Many Requests')) <p class="font-mono text-6xl font-semibold text-warning">429</p>
<h1 class="mt-4 font-bold tracking-tight text-white">Woah, slow down there!</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">You're making too many requests. Please wait a few seconds before trying again.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact
support
<span aria-hidden="true">&rarr;</span></a>
</div>
</div>
</div>
</div>

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