mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-24 20:49:32 +00:00
Compare commits
71 Commits
v4.0.0-bet
...
v4.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4cc406114 | ||
|
|
53b15a5762 | ||
|
|
929a4e6474 | ||
|
|
45b597bbab | ||
|
|
0d1a2aa5d1 | ||
|
|
b82353d5e2 | ||
|
|
b17c09f7a7 | ||
|
|
f6c3fe7888 | ||
|
|
2e855e030f | ||
|
|
49f86621f4 | ||
|
|
03d9f93397 | ||
|
|
c472042a94 | ||
|
|
9f4356f67d | ||
|
|
a50317cc76 | ||
|
|
8afa98a1ca | ||
|
|
f6737f21dd | ||
|
|
e4a51cc116 | ||
|
|
acd78ae196 | ||
|
|
953bcfb5bb | ||
|
|
dacfab8b29 | ||
|
|
48b3e99939 | ||
|
|
41ad67c7c9 | ||
|
|
b49725cb1c | ||
|
|
75e674a966 | ||
|
|
9d826d9fb4 | ||
|
|
0af221f9fc | ||
|
|
9066c9bf90 | ||
|
|
77e3208f00 | ||
|
|
db5ecf07bd | ||
|
|
7f28aa6985 | ||
|
|
9d53e04ce9 | ||
|
|
3db9a1dd6e | ||
|
|
38f9761b67 | ||
|
|
7117ecf634 | ||
|
|
522e20f10a | ||
|
|
ebbce2396c | ||
|
|
f9a2ff6d90 | ||
|
|
df1b9e7319 | ||
|
|
e7c0c26b32 | ||
|
|
0dbb8b4420 | ||
|
|
a4b44bacc1 | ||
|
|
1338e68b8c | ||
|
|
f6f4cdde24 | ||
|
|
3daadf13c6 | ||
|
|
cbd5fab2e7 | ||
|
|
48ccb508f9 | ||
|
|
67edce0612 | ||
|
|
e8a41d7e6e | ||
|
|
be1dad03bd | ||
|
|
31db1db636 | ||
|
|
dca332a688 | ||
|
|
35f19ed53f | ||
|
|
a5c45ffe90 | ||
|
|
b6c3a65d2d | ||
|
|
220c8211fd | ||
|
|
ffbd04df29 | ||
|
|
73c59be865 | ||
|
|
3966abaf80 | ||
|
|
759517316a | ||
|
|
3e3024d47e | ||
|
|
517cb77637 | ||
|
|
14bd89a991 | ||
|
|
304de29924 | ||
|
|
ab3055150f | ||
|
|
6b9c7aa9c5 | ||
|
|
040f47b59c | ||
|
|
eac7834083 | ||
|
|
135a298080 | ||
|
|
0065a86371 | ||
|
|
2a842a2f50 | ||
|
|
231c02e00e |
@@ -6,6 +6,7 @@
|
||||
USERID=
|
||||
GROUPID=
|
||||
############################################################################################################
|
||||
APP_NAME=Coolify-localhost
|
||||
APP_ID=development
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
|
||||
26
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
Normal file
26
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Bug report
|
||||
description: Create a new bug report
|
||||
title: '[Bug]: '
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of the problem
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Minimal Reproduction (if possible, example repository)
|
||||
description: Please provide a step by step guide to reproduce the issue
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Exception or Error
|
||||
description: Please provide error logs if possible.
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: Coolify's version (see bottom left corner).
|
||||
validations:
|
||||
required: true
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🤔 Community Support (Chat)
|
||||
url: https://coollabs.io/discord
|
||||
about: Reach out to us on Discord.
|
||||
- name: 🙋♂️ Feature Requests
|
||||
url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests-ideas
|
||||
about: All feature requests will be discussed here.
|
||||
84
.github/workflows/coolify-helper-next.yml
vendored
Normal file
84
.github/workflows/coolify-helper-next.yml
vendored
Normal 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 }}
|
||||
8
.github/workflows/coolify-helper.yml
vendored
8
.github/workflows/coolify-helper.yml
vendored
@@ -2,7 +2,7 @@ name: Coolify Helper Image (v4)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main", "next" ]
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- .github/workflows/coolify-helper.yml
|
||||
- docker/coolify-helper/Dockerfile
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
file: docker/coolify-helper/Dockerfile
|
||||
platforms: linux/aarch64
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:aarch64
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -78,3 +78,7 @@ jobs:
|
||||
- name: Create & publish manifest
|
||||
run: |
|
||||
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 }}
|
||||
|
||||
5
.github/workflows/development-build.yml
vendored
5
.github/workflows/development-build.yml
vendored
@@ -3,6 +3,9 @@ name: Development Build (v4)
|
||||
on:
|
||||
push:
|
||||
branches: ["next"]
|
||||
paths-ignore:
|
||||
- .github/workflows/coolify-helper.yml
|
||||
- docker/coolify-helper/Dockerfile
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
@@ -73,4 +76,4 @@ jobs:
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,3 +31,4 @@ _ide_helper.php
|
||||
_ide_helper_models.php
|
||||
.rnd
|
||||
/.ssh
|
||||
scripts/load-test/*
|
||||
|
||||
52
README.md
52
README.md
@@ -1,20 +1,30 @@
|
||||
# Coolify v4 Beta
|
||||
# About the Project
|
||||
|
||||
An open-source & self-hostable Heroku / Netlify alternative.
|
||||
Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
|
||||
|
||||
It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything.
|
||||
|
||||
Image if you could have the ease of a cloud but with your own servers. That is **Coolify**.
|
||||
|
||||
No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️
|
||||
|
||||
For more information, take a look at our landing page [here](https://coolify.io).
|
||||
|
||||
> If you are looking for previous (v3) version, it is [here](https://github.com/coollabsio/coolify/tree/v3).
|
||||
|
||||
# Cloud
|
||||
|
||||
If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io
|
||||
|
||||
You can easily attach your own servers, get all the automations, free email notifications, etc.
|
||||
|
||||
For more information & pricing, take a look at our landing page [here](https://coolify.io).
|
||||
|
||||
# Beta
|
||||
|
||||
You are checking the next-gen of Coolify, aka v4. Hi 👋
|
||||
The latest version (v4) is still in beta. That does not mean it is unstable. All the features that are available are stable enough be usable in real-life.
|
||||
|
||||
It is still in beta, lots of improvements will come every day. Things could break, but we are working hard to make it stable as soon as possible. If you find any bugs, please report them.
|
||||
|
||||
Automatic updates are available, so you will receive the latest version as soon as it is released.
|
||||
|
||||
If you are looking for v3, check out the [v3 branch](https://github.com/coollabsio/coolify/tree/v3).
|
||||
|
||||
## What's new?
|
||||
|
||||
Well, the whole tech stack changed, core is different, so yeah, a lot (documentation incoming).
|
||||
There are hundreds of people using it for managing their client's applications, freelancers, hobbyists, businesses.
|
||||
|
||||
# Installation
|
||||
|
||||
@@ -26,13 +36,19 @@ You can find the installation script [here](./scripts/install.sh).
|
||||
|
||||
## Support
|
||||
|
||||
- Twitter: [@heyandras](https://twitter.com/heyandras)
|
||||
- Mastodon: [@andrasbacsai@fosstodon.org](https://fosstodon.org/@andrasbacsai)
|
||||
- Email: [andras@coollabs.io](mailto:andras@coollabs.io)
|
||||
- Discord: [Invitation](https://coollabs.io/discord)
|
||||
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
|
||||
Contact us [here](https://docs.coollabs.io/contact).
|
||||
|
||||
---
|
||||
## Recognitions
|
||||
|
||||
<a href="https://news.ycombinator.com/item?id=26624341">
|
||||
<img
|
||||
style="width: 250px; height: 54px;" width="250" height="54"
|
||||
alt="Featured on Hacker News"
|
||||
src="https://hackernews-badge.vercel.app/api?id=26624341"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a href="https://www.producthunt.com/posts/coolify?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-coolify" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=338273&theme=light" alt="Coolify - An open-source & self-hostable Heroku, Netlify alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
## 💰 Financial Contributors
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ class StartPostgresql
|
||||
],
|
||||
'networks' => [
|
||||
$this->database->destination->network => [
|
||||
'external' => false,
|
||||
'external' => true,
|
||||
'name' => $this->database->destination->network,
|
||||
'attachable' => true,
|
||||
]
|
||||
|
||||
@@ -10,8 +10,14 @@ class InstallDocker
|
||||
{
|
||||
public function __invoke(Server $server, Team $team)
|
||||
{
|
||||
$dockerVersion = '23.0';
|
||||
$config = base64_encode('{ "live-restore": true }');
|
||||
$dockerVersion = '24.0';
|
||||
$config = base64_encode('{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
}
|
||||
}');
|
||||
if (isDev()) {
|
||||
$activity = remote_process([
|
||||
"echo ####### Installing Prerequisites...",
|
||||
@@ -34,7 +40,7 @@ class InstallDocker
|
||||
"echo ####### Restarting Docker Engine...",
|
||||
"systemctl restart docker",
|
||||
"echo ####### Creating default network...",
|
||||
"docker network create --attachable coolify",
|
||||
"docker network create --attachable coolify >/dev/null 2>&1 || true",
|
||||
"echo ####### Done!"
|
||||
], $server);
|
||||
$found = StandaloneDocker::where('server_id', $server->id);
|
||||
|
||||
@@ -23,6 +23,7 @@ use Mail;
|
||||
use Str;
|
||||
|
||||
use function Laravel\Prompts\select;
|
||||
use function Laravel\Prompts\text;
|
||||
|
||||
class TestEmail extends Command
|
||||
{
|
||||
@@ -44,9 +45,10 @@ class TestEmail extends Command
|
||||
* Execute the console command.
|
||||
*/
|
||||
private ?MailMessage $mail = null;
|
||||
private string $email = 'andras.bacsai@protonmail.com';
|
||||
public function handle()
|
||||
{
|
||||
$email = select(
|
||||
$type = select(
|
||||
'Which Email should be sent?',
|
||||
options: [
|
||||
'emails-test' => 'Test',
|
||||
@@ -60,15 +62,15 @@ class TestEmail extends Command
|
||||
'waitlist-confirmation' => 'Waitlist Confirmation',
|
||||
],
|
||||
);
|
||||
$type = set_transanctional_email_settings();
|
||||
if (!$type) {
|
||||
throw new Exception('No email settings found.');
|
||||
}
|
||||
$this->email = text('Email Address to send to');
|
||||
set_transanctional_email_settings();
|
||||
|
||||
$this->mail = new MailMessage();
|
||||
$this->mail->subject("Test Email");
|
||||
switch ($email) {
|
||||
switch ($type) {
|
||||
case 'emails-test':
|
||||
$this->mail = (new Test())->toMail();
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'application-deployment-success':
|
||||
$application = Application::all()->first();
|
||||
@@ -172,11 +174,7 @@ class TestEmail extends Command
|
||||
[],
|
||||
[],
|
||||
fn (Message $message) => $message
|
||||
->from(
|
||||
'internal@example.com',
|
||||
'Test Email',
|
||||
)
|
||||
->to('test@example.com')
|
||||
->to($this->email)
|
||||
->subject($this->mail->subject)
|
||||
->html((string)$this->mail->render())
|
||||
);
|
||||
|
||||
@@ -6,20 +6,20 @@ use App\Models\User;
|
||||
use App\Models\Waitlist;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class InviteFromWaitlist extends Command
|
||||
class WaitlistInvite extends Command
|
||||
{
|
||||
public Waitlist|null $next_patient = null;
|
||||
public User|null $new_user = null;
|
||||
public Waitlist|User|null $next_patient = null;
|
||||
public string|null $password = null;
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:invite-from-waitlist {email?}';
|
||||
protected $signature = 'waitlist:invite {email?} {--only-email}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -34,7 +34,16 @@ class InviteFromWaitlist extends Command
|
||||
public function handle()
|
||||
{
|
||||
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) {
|
||||
$this->error("{$this->argument('email')} not found in the waitlist.");
|
||||
return;
|
||||
@@ -43,6 +52,10 @@ class InviteFromWaitlist extends Command
|
||||
$this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first();
|
||||
}
|
||||
if ($this->next_patient) {
|
||||
if ($this->option('only-email')) {
|
||||
$this->send_email();
|
||||
return;
|
||||
}
|
||||
$this->register_user();
|
||||
$this->remove_from_waitlist();
|
||||
$this->send_email();
|
||||
@@ -55,7 +68,7 @@ class InviteFromWaitlist extends Command
|
||||
$already_registered = User::whereEmail($this->next_patient->email)->first();
|
||||
if (!$already_registered) {
|
||||
$this->password = Str::password();
|
||||
$this->new_user = User::create([
|
||||
User::create([
|
||||
'name' => Str::of($this->next_patient->email)->before('@'),
|
||||
'email' => $this->next_patient->email,
|
||||
'password' => Hash::make($this->password),
|
||||
@@ -73,10 +86,14 @@ class InviteFromWaitlist extends Command
|
||||
}
|
||||
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->view('emails.waitlist-invitation', [
|
||||
'email' => $this->next_patient->email,
|
||||
'password' => $this->password,
|
||||
'loginLink' => $loginLink,
|
||||
]);
|
||||
$mail->subject('Congratulations! You are invited to join Coolify Cloud.');
|
||||
send_user_an_email($mail, $this->next_patient->email);
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Jobs\ApplicationContainerStatusJob;
|
||||
use App\Jobs\CheckResaleLicenseJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\DatabaseContainerStatusJob;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\InstanceAutoUpdateJob;
|
||||
use App\Jobs\ProxyCheckJob;
|
||||
use App\Jobs\ResourceStatusJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
@@ -17,28 +22,46 @@ class Kernel extends ConsoleKernel
|
||||
{
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
// $schedule->call(fn() => $this->check_scheduled_backups($schedule))->everyTenSeconds();
|
||||
if (isDev()) {
|
||||
$schedule->command('horizon:snapshot')->everyMinute();
|
||||
$schedule->job(new ResourceStatusJob)->everyMinute();
|
||||
// $schedule->job(new ResourceStatusJob)->everyMinute();
|
||||
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
|
||||
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
|
||||
|
||||
// $schedule->job(new CheckResaleLicenseJob)->hourly();
|
||||
$schedule->job(new DockerCleanupJob)->everyOddHour();
|
||||
// $schedule->job(new InstanceAutoUpdateJob(true))->everyMinute();
|
||||
} else {
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
|
||||
$schedule->job(new ResourceStatusJob)->everyMinute()->onOneServer();
|
||||
$schedule->job(new CleanupInstanceStuffsJob)->everyTenMinutes()->onOneServer();
|
||||
// $schedule->job(new ResourceStatusJob)->everyMinute()->onOneServer();
|
||||
$schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
|
||||
$schedule->job(new ProxyCheckJob)->everyFiveMinutes()->onOneServer();
|
||||
$schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer();
|
||||
$schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes();
|
||||
}
|
||||
$this->instance_auto_update($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)
|
||||
{
|
||||
ray('check_scheduled_backups');
|
||||
@@ -57,7 +80,7 @@ class Kernel extends ConsoleKernel
|
||||
}
|
||||
$schedule->job(new DatabaseBackupJob(
|
||||
backup: $scheduled_backup
|
||||
))->cron($scheduled_backup->frequency);
|
||||
))->cron($scheduled_backup->frequency)->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Enums;
|
||||
|
||||
enum ProxyTypes: string
|
||||
{
|
||||
case NONE = 'NONE';
|
||||
case TRAEFIK_V2 = 'TRAEFIK_V2';
|
||||
case NGINX = 'NGINX';
|
||||
case CADDY = 'CADDY';
|
||||
|
||||
@@ -8,15 +8,41 @@ use App\Models\S3Storage;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\TeamInvitation;
|
||||
use App\Models\User;
|
||||
use Auth;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Throwable;
|
||||
use Str;
|
||||
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
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()
|
||||
{
|
||||
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');
|
||||
}
|
||||
public function boarding() {
|
||||
public function boarding()
|
||||
{
|
||||
if (currentTeam()->boarding || isDev()) {
|
||||
return view('boarding');
|
||||
} else {
|
||||
|
||||
@@ -60,9 +60,6 @@ class ProjectController extends Controller
|
||||
'environment_name' => $environment->name,
|
||||
'database_uuid' => $standalone_postgresql->uuid,
|
||||
]);
|
||||
}
|
||||
if ($server) {
|
||||
|
||||
}
|
||||
return view('project.new', [
|
||||
'type' => $type
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Actions\Server\InstallDocker;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -70,9 +71,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
}
|
||||
public function skipBoarding()
|
||||
{
|
||||
currentTeam()->update([
|
||||
Team::find(currentTeam()->id)->update([
|
||||
'show_boarding' => false
|
||||
]);
|
||||
ray(currentTeam());
|
||||
refreshSession();
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
@@ -25,6 +25,15 @@ class Dashboard extends Component
|
||||
}
|
||||
$this->projects = $projects->count();
|
||||
}
|
||||
// public function getIptables()
|
||||
// {
|
||||
// $servers = Server::ownedByCurrentTeam()->get();
|
||||
// foreach ($servers as $server) {
|
||||
// checkRequiredCommands($server);
|
||||
// $iptables = instant_remote_process(['docker run --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c "iptables -L -n | jc --iptables"'], $server);
|
||||
// ray($iptables);
|
||||
// }
|
||||
// }
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.dashboard');
|
||||
|
||||
@@ -18,22 +18,26 @@ class ForcePasswordReset extends Component
|
||||
'password' => 'required|min:8',
|
||||
'password_confirmation' => 'required|same:password',
|
||||
];
|
||||
public function mount() {
|
||||
public function mount()
|
||||
{
|
||||
$this->email = auth()->user()->email;
|
||||
}
|
||||
public function submit() {
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->rateLimit(10);
|
||||
$this->validate();
|
||||
$firstLogin = auth()->user()->created_at == auth()->user()->updated_at;
|
||||
auth()->user()->forceFill([
|
||||
'password' => Hash::make($this->password),
|
||||
'force_password_reset' => false,
|
||||
])->save();
|
||||
auth()->logout();
|
||||
return redirect()->route('login')->with('status', 'Your initial password has been set.');
|
||||
} catch(\Exception $e) {
|
||||
return general_error_handler(err:$e, that:$this);
|
||||
if ($firstLogin) {
|
||||
send_internal_notification('First login for ' . auth()->user()->email);
|
||||
}
|
||||
return redirect()->route('dashboard');
|
||||
} catch (\Exception $e) {
|
||||
return general_error_handler(err: $e, that: $this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
54
app/Http/Livewire/Help.php
Normal file
54
app/Http/Livewire/Help.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -8,26 +8,30 @@ use Livewire\Component;
|
||||
|
||||
class DiscordSettings extends Component
|
||||
{
|
||||
public Team $model;
|
||||
public Team $team;
|
||||
protected $rules = [
|
||||
'model.discord_enabled' => 'nullable|boolean',
|
||||
'model.discord_webhook_url' => 'required|url',
|
||||
'model.discord_notifications_test' => 'nullable|boolean',
|
||||
'model.discord_notifications_deployments' => 'nullable|boolean',
|
||||
'model.discord_notifications_status_changes' => 'nullable|boolean',
|
||||
'model.discord_notifications_database_backups' => 'nullable|boolean',
|
||||
'team.discord_enabled' => 'nullable|boolean',
|
||||
'team.discord_webhook_url' => 'required|url',
|
||||
'team.discord_notifications_test' => 'nullable|boolean',
|
||||
'team.discord_notifications_deployments' => 'nullable|boolean',
|
||||
'team.discord_notifications_status_changes' => 'nullable|boolean',
|
||||
'team.discord_notifications_database_backups' => 'nullable|boolean',
|
||||
];
|
||||
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()
|
||||
{
|
||||
try {
|
||||
$this->submit();
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
$this->model->discord_enabled = false;
|
||||
$this->team->discord_enabled = false;
|
||||
$this->validate();
|
||||
}
|
||||
}
|
||||
@@ -41,8 +45,8 @@ class DiscordSettings extends Component
|
||||
|
||||
public function saveModel()
|
||||
{
|
||||
$this->model->save();
|
||||
if (is_a($this->model, Team::class)) {
|
||||
$this->team->save();
|
||||
if (is_a($this->team, Team::class)) {
|
||||
refreshSession();
|
||||
}
|
||||
$this->emit('success', 'Settings saved.');
|
||||
@@ -50,7 +54,7 @@ class DiscordSettings extends Component
|
||||
|
||||
public function sendTestNotification()
|
||||
{
|
||||
$this->model->notify(new Test);
|
||||
$this->team->notify(new Test());
|
||||
$this->emit('success', 'Test notification sent.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ class EmailSettings extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->team = auth()->user()->currentTeam();
|
||||
['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits;
|
||||
$this->emails = auth()->user()->email;
|
||||
}
|
||||
@@ -106,7 +107,14 @@ class EmailSettings extends Component
|
||||
return general_error_handler($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function saveModel()
|
||||
{
|
||||
$this->team->save();
|
||||
if (is_a($this->team, Team::class)) {
|
||||
refreshSession();
|
||||
}
|
||||
$this->emit('success', 'Settings saved.');
|
||||
}
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
|
||||
66
app/Http/Livewire/Notifications/TelegramSettings.php
Normal file
66
app/Http/Livewire/Notifications/TelegramSettings.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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',
|
||||
'team.telegram_notifications_test_message_thread_id' => 'nullable|string',
|
||||
'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string',
|
||||
'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string',
|
||||
'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string',
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
'team.telegram_token' => 'Token',
|
||||
'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.');
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class Change extends Component
|
||||
if ($this->private_key->isEmpty()) {
|
||||
$this->private_key->delete();
|
||||
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.');
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -42,8 +42,13 @@ class Heading extends Component
|
||||
["docker rm -f {$this->database->uuid}"],
|
||||
$this->database->destination->server
|
||||
);
|
||||
if ($this->database->is_public) {
|
||||
stopPostgresProxy($this->database);
|
||||
$this->database->is_public = false;
|
||||
}
|
||||
$this->database->status = 'stopped';
|
||||
$this->database->save();
|
||||
$this->emit('refresh');
|
||||
// $this->database->environment->project->team->notify(new StatusChanged($this->database));
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ class General extends Component
|
||||
public StandalonePostgresql $database;
|
||||
public string $new_filename;
|
||||
public string $new_content;
|
||||
public string $db_url;
|
||||
|
||||
protected $listeners = ['refresh', 'save_init_script', 'delete_init_script'];
|
||||
|
||||
@@ -26,6 +27,8 @@ class General extends Component
|
||||
'database.init_scripts' => 'nullable',
|
||||
'database.image' => 'required',
|
||||
'database.ports_mappings' => 'nullable',
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
'database.name' => 'Name',
|
||||
@@ -38,8 +41,44 @@ class General extends Component
|
||||
'database.init_scripts' => 'Init Scripts',
|
||||
'database.image' => 'Image',
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
];
|
||||
public function mount()
|
||||
{
|
||||
$this->getDbUrl();
|
||||
}
|
||||
public function getDbUrl() {
|
||||
if ($this->database->is_public) {
|
||||
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->ip}:{$this->database->public_port}/{$this->database->postgres_db}";
|
||||
} else {
|
||||
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}";
|
||||
}
|
||||
}
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
if ($this->database->is_public && !$this->database->public_port) {
|
||||
$this->emit('error', 'Public port is required.');
|
||||
$this->database->is_public = false;
|
||||
return;
|
||||
}
|
||||
if ($this->database->is_public) {
|
||||
$this->emit('success', 'Starting TCP proxy...');
|
||||
startPostgresProxy($this->database);
|
||||
$this->emit('success', 'Database is now publicly accessible.');
|
||||
} else {
|
||||
stopPostgresProxy($this->database);
|
||||
$this->emit('success', 'Database is no longer publicly accessible.');
|
||||
}
|
||||
$this->getDbUrl();
|
||||
$this->database->save();
|
||||
} catch(Exception $e) {
|
||||
$this->database->is_public = !$this->database->is_public;
|
||||
return general_error_handler(err: $e, that: $this);
|
||||
}
|
||||
|
||||
}
|
||||
public function save_init_script($script)
|
||||
{
|
||||
$this->database->init_scripts = filter($this->database->init_scripts, fn ($s) => $s['filename'] !== $script['filename']);
|
||||
|
||||
@@ -22,34 +22,52 @@ class Select extends Component
|
||||
public Collection|array $swarmDockers = [];
|
||||
public array $parameters;
|
||||
|
||||
public ?string $existingPostgresqlUrl = null;
|
||||
|
||||
protected $queryString = [
|
||||
'server',
|
||||
];
|
||||
public function mount()
|
||||
{
|
||||
$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;
|
||||
if ($type === "existing-postgresql") {
|
||||
$this->current_step = $type;
|
||||
return;
|
||||
}
|
||||
if (count($this->servers) === 1) {
|
||||
$server = $this->servers->first();
|
||||
$this->set_server($server);
|
||||
$this->setServer($server);
|
||||
if (count($server->destinations()) === 1) {
|
||||
$this->set_destination($server->destinations()->first()->uuid);
|
||||
$this->setDestination($server->destinations()->first()->uuid);
|
||||
}
|
||||
}
|
||||
if (!is_null($this->server)) {
|
||||
$foundServer = $this->servers->where('id', $this->server)->first();
|
||||
if ($foundServer) {
|
||||
return $this->set_server($foundServer);
|
||||
return $this->setServer($foundServer);
|
||||
}
|
||||
}
|
||||
$this->current_step = 'servers';
|
||||
}
|
||||
|
||||
public function set_server(Server $server)
|
||||
public function setServer(Server $server)
|
||||
{
|
||||
$this->server_id = $server->id;
|
||||
$this->standaloneDockers = $server->standaloneDockers;
|
||||
@@ -57,7 +75,7 @@ class Select extends Component
|
||||
$this->current_step = 'destinations';
|
||||
}
|
||||
|
||||
public function set_destination(string $destination_uuid)
|
||||
public function setDestination(string $destination_uuid)
|
||||
{
|
||||
$this->destination_uuid = $destination_uuid;
|
||||
redirect()->route('project.resources.new', [
|
||||
|
||||
@@ -5,21 +5,84 @@ namespace App\Http\Livewire\Project\Shared\EnvironmentVariable;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
use Str;
|
||||
|
||||
class All extends Component
|
||||
{
|
||||
public $resource;
|
||||
public bool $showPreview = false;
|
||||
public string|null $modalId = null;
|
||||
public ?string $variables = null;
|
||||
public ?string $variablesPreview = null;
|
||||
public string $view = 'normal';
|
||||
protected $listeners = ['refreshEnvs', 'submit'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$resourceClass = get_class($this->resource);
|
||||
$resourceWithPreviews = ['App\Models\Application'];
|
||||
$simpleDockerfile = !is_null(data_get($this->resource, 'dockerfile'));
|
||||
if (Str::of($resourceClass)->contains($resourceWithPreviews) && !$simpleDockerfile) {
|
||||
$this->showPreview = true;
|
||||
}
|
||||
$this->modalId = new Cuid2(7);
|
||||
$this->getDevView();
|
||||
}
|
||||
public function getDevView()
|
||||
{
|
||||
$this->variables = $this->resource->environment_variables->map(function ($item) {
|
||||
return "$item->key=$item->value";
|
||||
})->sort()->join('
|
||||
');
|
||||
if ($this->showPreview) {
|
||||
$this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) {
|
||||
return "$item->key=$item->value";
|
||||
})->sort()->join('
|
||||
');
|
||||
}
|
||||
}
|
||||
public function switch()
|
||||
{
|
||||
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
|
||||
}
|
||||
public function saveVariables($isPreview)
|
||||
{
|
||||
if ($isPreview) {
|
||||
$variables = parseEnvFormatToArray($this->variablesPreview);
|
||||
$existingVariables = $this->resource->environment_variables_preview();
|
||||
$this->resource->environment_variables_preview()->delete();
|
||||
} else {
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
$existingVariables = $this->resource->environment_variables();
|
||||
$this->resource->environment_variables()->delete();
|
||||
}
|
||||
foreach ($variables as $key => $variable) {
|
||||
$found = $existingVariables->where('key', $key)->first();
|
||||
if ($found) {
|
||||
$found->value = $variable;
|
||||
$found->save();
|
||||
continue;
|
||||
} else {
|
||||
$environment = new EnvironmentVariable();
|
||||
$environment->key = $key;
|
||||
$environment->value = $variable;
|
||||
$environment->is_build_time = false;
|
||||
$environment->is_preview = $isPreview ? true : false;
|
||||
if ($this->resource->type() === 'application') {
|
||||
$environment->application_id = $this->resource->id;
|
||||
}
|
||||
if ($this->resource->type() === 'standalone-postgresql') {
|
||||
$environment->standalone_postgresql_id = $this->resource->id;
|
||||
}
|
||||
$environment->save();
|
||||
}
|
||||
}
|
||||
$this->refreshEnvs();
|
||||
}
|
||||
|
||||
public function refreshEnvs()
|
||||
{
|
||||
$this->resource->refresh();
|
||||
$this->getDevView();
|
||||
}
|
||||
|
||||
public function submit($data)
|
||||
@@ -43,7 +106,7 @@ class All extends Component
|
||||
$environment->standalone_postgresql_id = $this->resource->id;
|
||||
}
|
||||
$environment->save();
|
||||
$this->resource->refresh();
|
||||
$this->refreshEnvs();
|
||||
$this->emit('success', 'Environment variable added successfully.');
|
||||
} catch (\Exception $e) {
|
||||
return general_error_handler(err: $e, that: $this);
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Http\Livewire\Server\New;
|
||||
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -67,6 +69,11 @@ class ByIp extends Component
|
||||
'port' => $this->port,
|
||||
'team_id' => currentTeam()->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->save();
|
||||
|
||||
@@ -12,7 +12,7 @@ class Proxy extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public ProxyTypes $selectedProxy = ProxyTypes::TRAEFIK_V2;
|
||||
public ?string $selectedProxy = null;
|
||||
public $proxy_settings = null;
|
||||
public string|null $redirect_url = null;
|
||||
|
||||
@@ -20,6 +20,7 @@ class Proxy extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->selectedProxy = $this->server->proxy->type;
|
||||
$this->redirect_url = $this->server->proxy->redirect_url;
|
||||
}
|
||||
|
||||
@@ -35,11 +36,12 @@ class Proxy extends Component
|
||||
$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->status = 'exited';
|
||||
$this->server->save();
|
||||
$this->selectedProxy = $this->server->proxy->type;
|
||||
$this->emit('proxyStatusUpdated');
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@ class Email extends Component
|
||||
$this->validate([
|
||||
'settings.resend_api_key' => 'required'
|
||||
]);
|
||||
$this->settings->smtp_enabled = false;
|
||||
$this->settings->save();
|
||||
$this->emit('success', 'Settings saved successfully.');
|
||||
} catch (\Exception $e) {
|
||||
@@ -68,9 +67,18 @@ class Email extends Component
|
||||
return general_error_handler($e, $this);
|
||||
}
|
||||
}
|
||||
public function instantSaveResend() {
|
||||
try {
|
||||
$this->settings->smtp_enabled = false;
|
||||
$this->submitResend();
|
||||
} catch (\Exception $e) {
|
||||
return general_error_handler($e, $this);
|
||||
}
|
||||
}
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->settings->resend_enabled = false;
|
||||
$this->submit();
|
||||
} catch (\Exception $e) {
|
||||
return general_error_handler($e, $this);
|
||||
@@ -89,7 +97,6 @@ class Email extends Component
|
||||
'settings.smtp_password' => 'nullable',
|
||||
'settings.smtp_timeout' => 'nullable',
|
||||
]);
|
||||
$this->settings->resend_enabled = false;
|
||||
$this->settings->save();
|
||||
$this->emit('success', 'Settings saved successfully.');
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -32,16 +32,19 @@ class Create extends Component
|
||||
"custom_port" => 'required|int',
|
||||
"is_system_wide" => 'required|bool',
|
||||
]);
|
||||
$github_app = GithubApp::create([
|
||||
$payload = [
|
||||
'name' => $this->name,
|
||||
'organization' => $this->organization,
|
||||
'api_url' => $this->api_url,
|
||||
'html_url' => $this->html_url,
|
||||
'custom_user' => $this->custom_user,
|
||||
'custom_port' => $this->custom_port,
|
||||
'is_system_wide' => $this->is_system_wide,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
];
|
||||
if (isCloud()) {
|
||||
$payload['is_system_wide'] = $this->is_system_wide;
|
||||
}
|
||||
$github_app = GithubApp::create($payload);
|
||||
if (session('from')) {
|
||||
session(['from' => session('from') + ['source_id' => $github_app->id]]);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ class PricingPlans extends Component
|
||||
'tax_id_collection' => [
|
||||
'enabled' => true,
|
||||
],
|
||||
'automatic_tax' => [
|
||||
'enabled' => true,
|
||||
],
|
||||
'mode' => 'subscription',
|
||||
'success_url' => route('dashboard', ['success' => true]),
|
||||
'cancel_url' => route('subscription.index', ['cancelled' => true]),
|
||||
|
||||
@@ -23,7 +23,7 @@ class SwitchTeam extends Component
|
||||
if (!$team_to_switch_to) {
|
||||
return;
|
||||
}
|
||||
session(['currentTeam' => $team_to_switch_to]);
|
||||
refreshSession($team_to_switch_to);
|
||||
return redirect(request()->header('Referer'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@ class CheckForcePasswordReset
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
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;
|
||||
if ($force_password_reset) {
|
||||
if ($request->routeIs('auth.force-password-reset') || $request->path() === 'livewire/message/force-password-reset') {
|
||||
|
||||
@@ -66,12 +66,7 @@ class ApplicationDeploymentJob implements ShouldQueue
|
||||
private $log_model;
|
||||
private Collection $saved_outputs;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [
|
||||
(new WithoutOverlapping("dockerimagejobs"))->shared(),
|
||||
];
|
||||
}
|
||||
public $tries = 1;
|
||||
public function __construct(int $application_deployment_queue_id)
|
||||
{
|
||||
ray()->clearScreen();
|
||||
@@ -181,8 +176,8 @@ class ApplicationDeploymentJob implements ShouldQueue
|
||||
$this->execute_in_builder("echo '$dockerfile_base64' | base64 -d > $this->workdir/Dockerfile")
|
||||
],
|
||||
);
|
||||
$this->build_image_name = "{$this->application->git_repository}:build";
|
||||
$this->production_image_name = "{$this->application->uuid}:latest";
|
||||
$this->build_image_name = Str::lower("{$this->application->git_repository}:build");
|
||||
$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();
|
||||
$this->generate_compose_file();
|
||||
$this->generate_build_env_variables();
|
||||
@@ -206,8 +201,8 @@ class ApplicationDeploymentJob implements ShouldQueue
|
||||
$tag = $tag->substr(0, 128);
|
||||
}
|
||||
|
||||
$this->build_image_name = "{$this->application->git_repository}:{$tag}-build";
|
||||
$this->production_image_name = "{$this->application->uuid}:{$tag}";
|
||||
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
|
||||
$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();
|
||||
|
||||
if (!$this->force_rebuild) {
|
||||
@@ -242,7 +237,7 @@ class ApplicationDeploymentJob implements ShouldQueue
|
||||
}
|
||||
private function health_check()
|
||||
{
|
||||
ray('New container name: ',$this->container_name);
|
||||
ray('New container name: ', $this->container_name);
|
||||
if ($this->container_name) {
|
||||
$counter = 0;
|
||||
$this->execute_remote_command(
|
||||
@@ -264,7 +259,7 @@ class ApplicationDeploymentJob implements ShouldQueue
|
||||
);
|
||||
$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')) {
|
||||
@@ -282,8 +277,8 @@ class ApplicationDeploymentJob implements ShouldQueue
|
||||
}
|
||||
private function deploy_pull_request()
|
||||
{
|
||||
$this->build_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}-build";
|
||||
$this->production_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}";
|
||||
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
|
||||
$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();
|
||||
$this->execute_remote_command([
|
||||
"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()
|
||||
{
|
||||
$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(
|
||||
[
|
||||
"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,
|
||||
],
|
||||
[
|
||||
@@ -494,7 +496,7 @@ class ApplicationDeploymentJob implements ShouldQueue
|
||||
],
|
||||
'networks' => [
|
||||
$this->destination->network => [
|
||||
'external' => false,
|
||||
'external' => true,
|
||||
'name' => $this->destination->network,
|
||||
'attachable' => true,
|
||||
]
|
||||
@@ -654,12 +656,12 @@ class ApplicationDeploymentJob implements ShouldQueue
|
||||
private function build_image()
|
||||
{
|
||||
$this->execute_remote_command([
|
||||
"echo -n 'Building docker image.'",
|
||||
"echo -n 'Building docker image for your application.'",
|
||||
]);
|
||||
|
||||
if ($this->application->settings->is_static) {
|
||||
$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}
|
||||
@@ -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("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 {
|
||||
$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) {
|
||||
$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],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,28 +24,32 @@ class DatabaseBackupJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public Team|null $team = null;
|
||||
public ?Team $team = null;
|
||||
public Server $server;
|
||||
public ScheduledDatabaseBackup|null $backup;
|
||||
public ?ScheduledDatabaseBackup $backup = null;
|
||||
public string $database_type;
|
||||
public StandalonePostgresql $database;
|
||||
public ?StandalonePostgresql $database = null;
|
||||
public string $database_status;
|
||||
|
||||
public string|null $container_name = null;
|
||||
public ScheduledDatabaseBackupExecution|null $backup_log = null;
|
||||
public ?string $container_name = null;
|
||||
public ?ScheduledDatabaseBackupExecution $backup_log = null;
|
||||
public string $backup_status;
|
||||
public string|null $backup_location = null;
|
||||
public ?string $backup_location = null;
|
||||
public string $backup_dir;
|
||||
public string $backup_file;
|
||||
public int $size = 0;
|
||||
public string|null $backup_output = null;
|
||||
public S3Storage|null $s3 = null;
|
||||
public ?string $backup_output = null;
|
||||
public ?S3Storage $s3 = null;
|
||||
|
||||
public function __construct($backup)
|
||||
{
|
||||
$this->backup = $backup;
|
||||
$this->team = Team::find($backup->team_id);
|
||||
$this->database = $this->backup->database;
|
||||
if (!$this->database) {
|
||||
ray('Database not found');
|
||||
return;
|
||||
}
|
||||
$this->database_type = $this->database->type();
|
||||
$this->server = $this->database->destination->server;
|
||||
$this->database_status = $this->database->status;
|
||||
@@ -77,7 +81,7 @@ class DatabaseBackupJob implements ShouldQueue
|
||||
$ip = Str::slug($this->server->ip);
|
||||
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
|
||||
}
|
||||
$this->backup_file = "/dumpall-" . Carbon::now()->timestamp . ".sql";
|
||||
$this->backup_file = "/pg_dump-" . Carbon::now()->timestamp . ".dump";
|
||||
$this->backup_location = $this->backup_dir . $this->backup_file;
|
||||
|
||||
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
||||
@@ -107,7 +111,7 @@ class DatabaseBackupJob implements ShouldQueue
|
||||
try {
|
||||
ray($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);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
@@ -9,7 +10,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DockerCleanupJob implements ShouldQueue
|
||||
{
|
||||
@@ -30,6 +30,11 @@ class DockerCleanupJob implements ShouldQueue
|
||||
}
|
||||
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 {
|
||||
ray()->showQueries()->color('orange');
|
||||
$servers = Server::all();
|
||||
|
||||
75
app/Jobs/SendMessageToTelegramJob.php
Normal file
75
app/Jobs/SendMessageToTelegramJob.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?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,
|
||||
public ?string $topicId = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
];
|
||||
if ($this->topicId) {
|
||||
$payload['message_thread_id'] = $this->topicId;
|
||||
}
|
||||
$response = Http::post($url, $payload);
|
||||
if ($response->failed()) {
|
||||
throw new \Exception('Telegram notification failed with ' . $response->status() . ' status code.' . $response->body());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ class Application extends BaseModel
|
||||
|
||||
public function environment_variables(): HasMany
|
||||
{
|
||||
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false);
|
||||
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->orderBy('key', 'asc');
|
||||
}
|
||||
|
||||
public function runtime_environment_variables(): HasMany
|
||||
@@ -127,7 +127,7 @@ class Application extends BaseModel
|
||||
|
||||
public function environment_variables_preview(): HasMany
|
||||
{
|
||||
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true);
|
||||
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc');
|
||||
}
|
||||
|
||||
public function runtime_environment_variables_preview(): HasMany
|
||||
|
||||
@@ -20,13 +20,16 @@ class EnvironmentVariable extends Model
|
||||
{
|
||||
static::created(function ($environment_variable) {
|
||||
if ($environment_variable->application_id && !$environment_variable->is_preview) {
|
||||
ModelsEnvironmentVariable::create([
|
||||
'key' => $environment_variable->key,
|
||||
'value' => $environment_variable->value,
|
||||
'is_build_time' => $environment_variable->is_build_time,
|
||||
'application_id' => $environment_variable->application_id,
|
||||
'is_preview' => true,
|
||||
]);
|
||||
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview',true)->first();
|
||||
if (!$found) {
|
||||
ModelsEnvironmentVariable::create([
|
||||
'key' => $environment_variable->key,
|
||||
'value' => $environment_variable->value,
|
||||
'is_build_time' => $environment_variable->is_build_time,
|
||||
'application_id' => $environment_variable->application_id,
|
||||
'is_preview' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,6 @@ class StandaloneDocker extends BaseModel
|
||||
|
||||
public function attachedTo()
|
||||
{
|
||||
return $this->applications->count() > 0 || $this->databases->count() > 0;
|
||||
return $this->applications?->count() > 0 || $this->databases?->count() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,14 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
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)
|
||||
{
|
||||
$recipients = data_get($notification, 'emails', null);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use App\Notifications\Channels\SendsEmail;
|
||||
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
|
||||
use Cache;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
@@ -94,7 +95,9 @@ class User extends Authenticatable implements SendsEmail
|
||||
|
||||
public function currentTeam()
|
||||
{
|
||||
return Team::find(session('currentTeam')->id);
|
||||
return Cache::remember('team:' . auth()->user()->id, 3600, function() {
|
||||
return Team::find(session('currentTeam')->id);
|
||||
});
|
||||
}
|
||||
|
||||
public function otherTeams()
|
||||
|
||||
@@ -16,6 +16,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 5;
|
||||
public Application $application;
|
||||
public string $deployment_uuid;
|
||||
public ?ApplicationPreview $preview = null;
|
||||
@@ -43,19 +44,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$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;
|
||||
return setNotificationChannels($notifiable, 'deployments');
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
@@ -89,4 +78,19 @@ class DeploymentFailed extends Notification implements ShouldQueue
|
||||
}
|
||||
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
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 5;
|
||||
public Application $application;
|
||||
public string $deployment_uuid;
|
||||
public ApplicationPreview|null $preview = null;
|
||||
@@ -43,19 +44,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$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;
|
||||
return setNotificationChannels($notifiable, 'deployments');
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
@@ -99,4 +88,34 @@ class DeploymentSuccess extends Notification implements ShouldQueue
|
||||
}
|
||||
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
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class StatusChanged extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 5;
|
||||
public $application;
|
||||
|
||||
public string $application_name;
|
||||
@@ -37,19 +38,7 @@ class StatusChanged extends Notification implements ShouldQueue
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$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;
|
||||
return setNotificationChannels($notifiable, 'status_changes');
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
@@ -70,7 +59,20 @@ class StatusChanged extends Notification implements ShouldQueue
|
||||
$message = '⛔ ' . $this->application_name . ' has been stopped.
|
||||
|
||||
';
|
||||
$message .= '[Application URL](' . $this->application_url . ')';
|
||||
$message .= '[Open Application in Coolify](' . $this->application_url . ')';
|
||||
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
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class EmailChannel
|
||||
{
|
||||
private bool $isResend = false;
|
||||
public function send(SendsEmail $notifiable, Notification $notification): void
|
||||
{
|
||||
$this->bootConfigs($notifiable);
|
||||
@@ -20,33 +19,14 @@ class EmailChannel
|
||||
}
|
||||
|
||||
$mailMessage = $notification->toMail($notifiable);
|
||||
// if ($this->isResend) {
|
||||
Mail::send(
|
||||
[],
|
||||
[],
|
||||
fn (Message $message) => $message
|
||||
->from(
|
||||
data_get($notifiable, 'smtp_from_address'),
|
||||
data_get($notifiable, 'smtp_from_name'),
|
||||
)
|
||||
->to($recepients)
|
||||
->subject($mailMessage->subject)
|
||||
->html((string)$mailMessage->render())
|
||||
);
|
||||
// } else {
|
||||
// Mail::send(
|
||||
// [],
|
||||
// [],
|
||||
// fn (Message $message) => $message
|
||||
// ->from(
|
||||
// data_get($notifiable, 'smtp_from_address'),
|
||||
// data_get($notifiable, 'smtp_from_name'),
|
||||
// )
|
||||
// ->bcc($recepients)
|
||||
// ->subject($mailMessage->subject)
|
||||
// ->html((string)$mailMessage->render())
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
private function bootConfigs($notifiable): void
|
||||
@@ -56,13 +36,11 @@ class EmailChannel
|
||||
if (!$type) {
|
||||
throw new Exception('No email settings found.');
|
||||
}
|
||||
if ($type === 'resend') {
|
||||
$this->isResend = true;
|
||||
}
|
||||
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')) {
|
||||
$this->isResend = true;
|
||||
config()->set('mail.default', 'resend');
|
||||
config()->set('resend.api_key', data_get($notifiable, 'resend_api_key'));
|
||||
}
|
||||
|
||||
9
app/Notifications/Channels/SendsTelegram.php
Normal file
9
app/Notifications/Channels/SendsTelegram.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications\Channels;
|
||||
|
||||
interface SendsTelegram
|
||||
{
|
||||
public function routeNotificationForTelegram();
|
||||
|
||||
}
|
||||
39
app/Notifications/Channels/TelegramChannel.php
Normal file
39
app/Notifications/Channels/TelegramChannel.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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', []);
|
||||
$telegramToken = data_get($telegramData, 'token');
|
||||
$chatId = data_get($telegramData, 'chat_id');
|
||||
$topicId = null;
|
||||
$topicsInstance = get_class($notification);
|
||||
|
||||
switch ($topicsInstance) {
|
||||
case 'App\Notifications\StatusChange':
|
||||
$topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id');
|
||||
break;
|
||||
case 'App\Notifications\Test':
|
||||
$topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id');
|
||||
break;
|
||||
case 'App\Notifications\Deployment':
|
||||
$topicId = data_get($notifiable, 'telegram_notifications_deployments_message_thread_id');
|
||||
break;
|
||||
case 'App\Notifications\DatabaseBackup':
|
||||
$topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id');
|
||||
break;
|
||||
}
|
||||
if (!$telegramToken || !$chatId || !$message) {
|
||||
return;
|
||||
}
|
||||
dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId));
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ use Log;
|
||||
|
||||
class TransactionalEmailChannel
|
||||
{
|
||||
private bool $isResend = false;
|
||||
public function send(User $notifiable, Notification $notification): void
|
||||
{
|
||||
$settings = InstanceSettings::get();
|
||||
@@ -26,33 +25,14 @@ class TransactionalEmailChannel
|
||||
}
|
||||
$this->bootConfigs();
|
||||
$mailMessage = $notification->toMail($notifiable);
|
||||
// if ($this->isResend) {
|
||||
Mail::send(
|
||||
[],
|
||||
[],
|
||||
fn (Message $message) => $message
|
||||
->from(
|
||||
data_get($settings, 'smtp_from_address'),
|
||||
data_get($settings, 'smtp_from_name'),
|
||||
)
|
||||
->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
|
||||
@@ -61,8 +41,5 @@ class TransactionalEmailChannel
|
||||
if (!$type) {
|
||||
throw new Exception('No email settings found.');
|
||||
}
|
||||
if ($type === 'resend') {
|
||||
$this->isResend = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class BackupFailed extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 5;
|
||||
public string $name;
|
||||
public string $frequency;
|
||||
|
||||
@@ -25,19 +26,7 @@ class BackupFailed extends Notification implements ShouldQueue
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$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;
|
||||
return setNotificationChannels($notifiable, 'database_backups');
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
@@ -56,4 +45,11 @@ class BackupFailed extends Notification implements ShouldQueue
|
||||
{
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class BackupSuccess extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 5;
|
||||
public string $name;
|
||||
public string $frequency;
|
||||
|
||||
@@ -25,19 +26,7 @@ class BackupSuccess extends Notification implements ShouldQueue
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$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;
|
||||
return setNotificationChannels($notifiable, 'database_backups');
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
@@ -55,4 +44,11 @@ class BackupSuccess extends Notification implements ShouldQueue
|
||||
{
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Notifications\Internal;
|
||||
|
||||
use App\Notifications\Channels\DiscordChannel;
|
||||
use App\Notifications\Channels\TelegramChannel;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Notification;
|
||||
@@ -11,17 +12,24 @@ class GeneralNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 5;
|
||||
public function __construct(public string $message)
|
||||
{}
|
||||
{
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels[] = DiscordChannel::class;
|
||||
return $channels;
|
||||
return [TelegramChannel::class, DiscordChannel::class];
|
||||
}
|
||||
|
||||
public function toDiscord(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
public function toTelegram(): array
|
||||
{
|
||||
return [
|
||||
"message" => $this->message,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class NotReachable extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 5;
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
|
||||
@@ -22,19 +23,7 @@ class NotReachable extends Notification implements ShouldQueue
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$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;
|
||||
return setNotificationChannels($notifiable, 'status_changes');
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
@@ -55,4 +44,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.';
|
||||
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.'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Notifications;
|
||||
|
||||
use App\Notifications\Channels\DiscordChannel;
|
||||
use App\Notifications\Channels\EmailChannel;
|
||||
use App\Notifications\Channels\TelegramChannel;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
@@ -13,24 +14,14 @@ class Test extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 5;
|
||||
public function __construct(public string|null $emails = null)
|
||||
{
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$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;
|
||||
return setNotificationChannels($notifiable, 'test');
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
@@ -48,4 +39,16 @@ class Test extends Notification implements ShouldQueue
|
||||
$message .= '[Go to your dashboard](' . base_url() . ')';
|
||||
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()
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class InvitationLink extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 5;
|
||||
public function via(): array
|
||||
{
|
||||
return [TransactionalEmailChannel::class];
|
||||
|
||||
@@ -50,10 +50,6 @@ class ResetPassword extends Notification
|
||||
protected function buildMailMessage($url)
|
||||
{
|
||||
$mail = new MailMessage();
|
||||
$mail->from(
|
||||
data_get($this->settings, 'smtp_from_address'),
|
||||
data_get($this->settings, 'smtp_from_name'),
|
||||
);
|
||||
$mail->subject('Reset Password');
|
||||
$mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]);
|
||||
return $mail;
|
||||
|
||||
@@ -12,6 +12,7 @@ class Test extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $tries = 5;
|
||||
public function __construct(public string $emails)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ function create_standalone_postgresql($environment_id, $destination_uuid): Stand
|
||||
}
|
||||
return StandalonePostgresql::create([
|
||||
'name' => generate_database_name('postgresql'),
|
||||
'postgres_password' => \Illuminate\Support\Str::password(),
|
||||
'postgres_password' => \Illuminate\Support\Str::password(symbols: false),
|
||||
'environment_id' => $environment_id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
function get_proxy_path()
|
||||
@@ -166,3 +167,75 @@ function setup_default_redirect_404(string|null $redirect_url, Server $server)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startPostgresProxy(StandalonePostgresql $database)
|
||||
{
|
||||
$containerName = "{$database->uuid}-proxy";
|
||||
$configuration_dir = database_proxy_dir($database->uuid);
|
||||
$nginxconf = <<<EOF
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
stream {
|
||||
server {
|
||||
listen $database->public_port;
|
||||
proxy_pass $database->uuid:5432;
|
||||
}
|
||||
}
|
||||
EOF;
|
||||
$docker_compose = [
|
||||
'version' => '3.8',
|
||||
'services' => [
|
||||
$containerName => [
|
||||
'image' => "nginx:stable-alpine",
|
||||
'container_name' => $containerName,
|
||||
'restart' => RESTART_MODE,
|
||||
'volumes' => [
|
||||
"$configuration_dir/nginx.conf:/etc/nginx/nginx.conf:ro",
|
||||
],
|
||||
'ports' => [
|
||||
"$database->public_port:$database->public_port",
|
||||
],
|
||||
'networks' => [
|
||||
$database->destination->network,
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => [
|
||||
'CMD-SHELL',
|
||||
'stat /etc/nginx/nginx.conf || exit 1',
|
||||
],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 3,
|
||||
'start_period' => '1s'
|
||||
],
|
||||
]
|
||||
],
|
||||
'networks' => [
|
||||
$database->destination->network => [
|
||||
'external' => true,
|
||||
'name' => $database->destination->network,
|
||||
'attachable' => true,
|
||||
]
|
||||
]
|
||||
];
|
||||
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
|
||||
$nginxconf_base64 = base64_encode($nginxconf);
|
||||
instant_remote_process([
|
||||
"mkdir -p $configuration_dir",
|
||||
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
|
||||
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
|
||||
"docker compose --project-directory {$configuration_dir} up -d >/dev/null",
|
||||
|
||||
|
||||
], $database->destination->server);
|
||||
}
|
||||
function stopPostgresProxy(StandalonePostgresql $database)
|
||||
{
|
||||
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ function generate_ssh_command(string $private_key_location, string $server_ip, s
|
||||
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 ';
|
||||
}
|
||||
$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} "
|
||||
. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
|
||||
. '-o PasswordAuthentication=no '
|
||||
@@ -92,7 +93,18 @@ function generate_ssh_command(string $private_key_location, string $server_ip, s
|
||||
|
||||
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)
|
||||
{
|
||||
$command_string = implode("\n", $command);
|
||||
@@ -216,3 +228,29 @@ function check_server_connection(Server $server)
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
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 DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Mail\Message;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -26,6 +28,10 @@ function database_configuration_dir(): string
|
||||
{
|
||||
return '/data/coolify/databases';
|
||||
}
|
||||
function database_proxy_dir($uuid): string
|
||||
{
|
||||
return "/data/coolify/databases/$uuid/proxy";
|
||||
}
|
||||
|
||||
function backup_dir(): string
|
||||
{
|
||||
@@ -51,9 +57,15 @@ function showBoarding(): bool
|
||||
{
|
||||
return currentTeam()->show_boarding ?? false;
|
||||
}
|
||||
function refreshSession(): void
|
||||
function refreshSession(?Team $team = null): void
|
||||
{
|
||||
$team = Team::find(currentTeam()->id);
|
||||
if (!$team) {
|
||||
$team = Team::find(currentTeam()->id);
|
||||
}
|
||||
Cache::forget('team:' . auth()->user()->id);
|
||||
Cache::remember('team:' . auth()->user()->id, 3600, function() use ($team) {
|
||||
return $team;
|
||||
});
|
||||
session(['currentTeam' => $team]);
|
||||
}
|
||||
function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed
|
||||
@@ -149,6 +161,8 @@ function set_transanctional_email_settings(InstanceSettings | null $settings = n
|
||||
if (!$settings) {
|
||||
$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')) {
|
||||
config()->set('mail.default', 'resend');
|
||||
config()->set('resend.api_key', data_get($settings, 'resend_api_key'));
|
||||
@@ -241,9 +255,9 @@ function validate_cron_expression($expression_to_validate): bool
|
||||
function send_internal_notification(string $message): void
|
||||
{
|
||||
try {
|
||||
$baseUrl = base_url(false);
|
||||
$baseUrl = config('app.name');
|
||||
$team = Team::find(0);
|
||||
$team->notify(new GeneralNotification("👀 Internal notifications from {$baseUrl}: " . $message));
|
||||
$team->notify(new GeneralNotification("👀 {$baseUrl}: " . $message));
|
||||
} catch (\Throwable $th) {
|
||||
ray($th->getMessage());
|
||||
}
|
||||
@@ -259,17 +273,54 @@ function send_user_an_email(MailMessage $mail, string $email): void
|
||||
[],
|
||||
[],
|
||||
fn (Message $message) => $message
|
||||
->from(
|
||||
data_get($settings, 'smtp_from_address'),
|
||||
data_get($settings, 'smtp_from_name')
|
||||
)
|
||||
->to($email)
|
||||
->subject($mail->subject)
|
||||
->html((string) $mail->render())
|
||||
);
|
||||
|
||||
}
|
||||
function isEmailEnabled($notifiable)
|
||||
{
|
||||
return data_get($notifiable, 'smtp_enabled') || data_get($notifiable, 'resend_enabled') || data_get($notifiable, 'use_instance_email_settings');
|
||||
}
|
||||
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;
|
||||
}
|
||||
function parseEnvFormatToArray($env_file_contents) {
|
||||
$env_array = array();
|
||||
$lines = explode("\n", $env_file_contents);
|
||||
foreach ($lines as $line) {
|
||||
if ($line === '' || substr($line, 0, 1) === '#') {
|
||||
continue;
|
||||
}
|
||||
$equals_pos = strpos($line, '=');
|
||||
if ($equals_pos !== false) {
|
||||
$key = substr($line, 0, $equals_pos);
|
||||
$value = substr($line, $equals_pos + 1);
|
||||
if (substr($value, 0, 1) === '"' && substr($value, -1) === '"') {
|
||||
$value = substr($value, 1, -1);
|
||||
}
|
||||
elseif (substr($value, 0, 1) === "'" && substr($value, -1) === "'") {
|
||||
$value = substr($value, 1, -1);
|
||||
}
|
||||
$env_array[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $env_array;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ function getEndDate()
|
||||
|
||||
function isSubscriptionActive()
|
||||
{
|
||||
if (!isCloud()) {
|
||||
return false;
|
||||
}
|
||||
$team = currentTeam();
|
||||
if (!$team) {
|
||||
return false;
|
||||
|
||||
@@ -7,4 +7,5 @@ return [
|
||||
'mux_enabled' => env('MUX_ENABLED', true),
|
||||
'dev_webhook' => env('SERVEO_URL'),
|
||||
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
||||
'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'),
|
||||
];
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
return [
|
||||
|
||||
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
|
||||
'dsn' => 'https://abe219b6573947128ecf523c835f5f38@o1082494.ingest.sentry.io/4505347448045568',
|
||||
'dsn' => 'https://62de992090e4e0cb28f18231835ea006@o1082494.ingest.sentry.io/4505347448045568',
|
||||
|
||||
// The release version of your application
|
||||
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
|
||||
'release' => '4.0.0-beta.23',
|
||||
'release' => '4.0.0-beta.29',
|
||||
'server_name' => env('APP_ID', 'coolify'),
|
||||
// When left empty or `null` the Laravel environment will be used
|
||||
'environment' => config('app.env'),
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<?php
|
||||
|
||||
return '4.0.0-beta.23';
|
||||
return '4.0.0-beta.29';
|
||||
|
||||
32
database/migrations/2023_08_22_071054_add_stripe_reasons.php
Normal file
32
database/migrations/2023_08_22_071054_add_stripe_reasons.php
Normal 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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('teams', function (Blueprint $table) {
|
||||
$table->text('telegram_notifications_test_message_thread_id')->nullable();
|
||||
$table->text('telegram_notifications_deployments_message_thread_id')->nullable();
|
||||
$table->text('telegram_notifications_status_changes_message_thread_id')->nullable();
|
||||
$table->text('telegram_notifications_database_backups_message_thread_id')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('teams', function (Blueprint $table) {
|
||||
$table->dropColumn('telegram_message_thread_id');
|
||||
$table->dropColumn('telegram_notifications_test_message_thread_id');
|
||||
$table->dropColumn('telegram_notifications_deployments_message_thread_id');
|
||||
$table->dropColumn('telegram_notifications_status_changes_message_thread_id');
|
||||
$table->dropColumn('telegram_notifications_database_backups_message_thread_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
$coolify_key_name = "id.root@host.docker.internal";
|
||||
$coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}");
|
||||
|
||||
@@ -6,6 +6,7 @@ x-testing-host: &testing-host-base
|
||||
context: ./docker/testing-host
|
||||
networks:
|
||||
- coolify
|
||||
init: true
|
||||
|
||||
services:
|
||||
coolify:
|
||||
@@ -53,6 +54,7 @@ services:
|
||||
<<: *testing-host-base
|
||||
container_name: coolify-testing-host
|
||||
volumes:
|
||||
- /:/host
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /data/coolify/:/data/coolify
|
||||
mailpit:
|
||||
|
||||
@@ -2,15 +2,15 @@ FROM alpine:3.17
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
# 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
|
||||
ARG DOCKER_COMPOSE_VERSION=2.18.1
|
||||
ARG DOCKER_COMPOSE_VERSION=2.21.0
|
||||
# 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
|
||||
ARG PACK_VERSION=0.29.0
|
||||
ARG PACK_VERSION=0.30.0
|
||||
# https://github.com/railwayapp/nixpacks/releases
|
||||
ARG NIXPACKS_VERSION=1.12.0
|
||||
ARG NIXPACKS_VERSION=1.13.0
|
||||
|
||||
USER root
|
||||
WORKDIR /artifacts
|
||||
@@ -38,5 +38,5 @@ COPY --from=minio/mc /usr/bin/mc /usr/bin/mc
|
||||
RUN chmod +x /usr/bin/mc
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD ["sh", "-c", "while true; do sleep 1; done"]
|
||||
CMD ["tail", "-f", "/dev/null"]
|
||||
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
FROM alpine:3.17
|
||||
FROM debian:12-slim
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
# 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
|
||||
ARG DOCKER_COMPOSE_VERSION=2.18.1
|
||||
ARG DOCKER_COMPOSE_VERSION=2.21.0
|
||||
# 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
|
||||
ARG PACK_VERSION=0.29.0
|
||||
ARG PACK_VERSION=0.30.0
|
||||
# https://github.com/railwayapp/nixpacks/releases
|
||||
ARG NIXPACKS_VERSION=1.12.0
|
||||
ARG NIXPACKS_VERSION=1.13.0
|
||||
|
||||
USER 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 if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
|
||||
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/compose/releases/download/v${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose && \
|
||||
(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://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
|
||||
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
|
||||
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
|
||||
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)
|
||||
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /root/.docker/cli-plugins/docker-buildx
|
||||
|
||||
|
||||
# Setup sshd
|
||||
RUN ssh-keygen -A
|
||||
@@ -32,6 +31,4 @@ RUN mkdir -p ~/.ssh
|
||||
RUN echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68 coolify@coolify-instance" >> ~/.ssh/authorized_keys
|
||||
|
||||
EXPOSE 22
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD ["/usr/sbin/sshd", "-D", "-o", "ListenAddress=0.0.0.0"]
|
||||
|
||||
|
||||
@@ -593,7 +593,7 @@ async function redirect() {
|
||||
targetUrl.pathname = `/source/new`
|
||||
break;
|
||||
case 7:
|
||||
targetUrl.pathname = `/private-key/new`
|
||||
targetUrl.pathname = `/security/private-key/new`
|
||||
break;
|
||||
case 8:
|
||||
targetUrl.pathname = `/destination/new`
|
||||
@@ -612,7 +612,7 @@ async function redirect() {
|
||||
targetUrl.pathname = `/servers`
|
||||
break;
|
||||
case 13:
|
||||
targetUrl.pathname = `/private-keys`
|
||||
targetUrl.pathname = `/security/private-key`
|
||||
break;
|
||||
case 14:
|
||||
targetUrl.pathname = `/projects`
|
||||
|
||||
@@ -51,6 +51,11 @@
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="mb-4 font-medium text-red-600">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{ Illuminate\Mail\Markdown::parse('---') }}
|
||||
|
||||
Thank you.<br>
|
||||
Thank you,<br>
|
||||
{{ config('app.name') ?? 'Coolify' }}
|
||||
|
||||
{{ Illuminate\Mail\Markdown::parse('[Contact Support](https://docs.coollabs.io)') }}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
@endif
|
||||
</span>
|
||||
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
@if ($instantSave) wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
|
||||
@if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
|
||||
wire:model.defer={{ $id }} @else wire:model.defer={{ $value ?? $id }} @endif />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
wire:model.defer={{ $id }} wire:dirty.class.remove='text-white'
|
||||
wire:dirty.class="input-warning" wire:loading.attr="disabled" type="{{ $type }}"
|
||||
@readonly($readonly) @disabled($disabled) id="{{ $id }}" name="{{ $name }}"
|
||||
placeholder="{{ $attributes->get('placeholder') }}">
|
||||
placeholder="{{ $attributes->get('placeholder') }}"
|
||||
aria-placeholder="{{ $attributes->get('placeholder') }}">
|
||||
|
||||
</div>
|
||||
@else
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="absolute hidden text-xs rounded group-hover:block border-coolgray-400 bg-coolgray-500">
|
||||
<div class="p-4 card-body">
|
||||
<div class="absolute z-40 hidden text-xs rounded group-hover:block border-coolgray-400 bg-coolgray-500">
|
||||
<div class="p-4">
|
||||
{!! $helper !!}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,10 +43,6 @@
|
||||
@endisset
|
||||
@if ($modalSubmit)
|
||||
{{ $modalSubmit }}
|
||||
@else
|
||||
<x-forms.button onclick="{{ $modalId }}.close()" type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
@endif
|
||||
|
||||
</form>
|
||||
|
||||
@@ -50,6 +50,23 @@
|
||||
</svg>
|
||||
</a>
|
||||
</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">
|
||||
<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"
|
||||
@@ -64,6 +81,7 @@
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<div class="flex-1"></div>
|
||||
@if (isInstanceAdmin() && !isCloud())
|
||||
<livewire:upgrade />
|
||||
@@ -95,6 +113,20 @@
|
||||
</a>
|
||||
</li>
|
||||
@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">
|
||||
<form action="/logout" method="POST" class=" hover:bg-transparent">
|
||||
@csrf
|
||||
|
||||
17
resources/views/components/security/navbar.blade.php
Normal file
17
resources/views/components/security/navbar.blade.php
Normal 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>
|
||||
5
resources/views/emails/help.blade.php
Normal file
5
resources/views/emails/help.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
{{ $description }}
|
||||
|
||||
{{ Illuminate\Mail\Markdown::parse('---') }}
|
||||
|
||||
{{ Illuminate\Mail\Markdown::parse($debug) }}
|
||||
@@ -1,5 +1,5 @@
|
||||
<x-emails.layout>
|
||||
A password reset has been requested for this email address on [{{ config('app.name') }}]({{ config('app.url') }}).
|
||||
A password reset has been requested for this email address.
|
||||
|
||||
Click [here]({{ $url }}) to reset your password.
|
||||
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
<x-emails.layout>
|
||||
You have been invited to join the Coolify Cloud.
|
||||
|
||||
[Login here]({{base_url()}}/login)
|
||||
|
||||
Here is your initial login information.
|
||||
|
||||
Email:
|
||||
|
||||
**{{ $email }}**
|
||||
|
||||
Initial Password:
|
||||
|
||||
**{{ $password }}**
|
||||
|
||||
(You will forced to change it on first login.)
|
||||
|
||||
You have been invited to join the Coolify Cloud: [Get Started]({{$loginLink}})
|
||||
</x-emails.layout>
|
||||
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Unauthorized'))
|
||||
@section('code', '401')
|
||||
@section('message', __('Unauthorized'))
|
||||
@extends('layouts.base')
|
||||
<div class="min-h-screen hero">
|
||||
<div class="text-center hero-content">
|
||||
<div class="">
|
||||
<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>
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Forbidden'))
|
||||
@section('code', '403')
|
||||
@section('message', __($exception->getMessage() ?: 'Forbidden'))
|
||||
@extends('layouts.base')
|
||||
<div class="min-h-screen hero">
|
||||
<div class="text-center hero-content">
|
||||
<div class="">
|
||||
<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>
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
<x-layout>
|
||||
<div class="min-h-screen hero">
|
||||
<div class="text-center hero-content">
|
||||
<div class="">
|
||||
<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>
|
||||
<p class="mt-6 text-base leading-7 text-neutral-300">Sorry, we couldn’t find the page you’re looking
|
||||
for.
|
||||
</p>
|
||||
<div class="flex items-center justify-center mt-10 gap-x-6">
|
||||
<a href="/">
|
||||
<x-forms.button isHighlighted>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>
|
||||
@extends('layouts.base')
|
||||
<div class="min-h-screen hero">
|
||||
<div class="text-center hero-content">
|
||||
<div class="">
|
||||
<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>
|
||||
<p class="mt-6 text-base leading-7 text-neutral-300">Sorry, we couldn’t find the page you’re looking
|
||||
for.
|
||||
</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>
|
||||
</x-layout>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
<x-layout>
|
||||
<div class="min-h-screen hero">
|
||||
<div class="text-center hero-content">
|
||||
<div class="">
|
||||
<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>
|
||||
<p class="mt-6 text-base leading-7 text-neutral-300">Sorry, we couldn’t find the page you’re looking
|
||||
for.
|
||||
</p>
|
||||
<div class="flex items-center justify-center mt-10 gap-x-6">
|
||||
<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
|
||||
back home</a>
|
||||
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact
|
||||
support
|
||||
<span aria-hidden="true">→</span></a>
|
||||
</div>
|
||||
@extends('layouts.base')
|
||||
<div class="min-h-screen hero">
|
||||
<div class="text-center hero-content">
|
||||
<div class="">
|
||||
<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, not like you!</h1>
|
||||
<p class="mt-6 text-base leading-7 text-neutral-300">Sorry, we couldn’t find the page you’re looking
|
||||
for.
|
||||
</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">→</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-layout>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Too Many Requests'))
|
||||
@section('code', '429')
|
||||
@section('message', __('Too Many Requests'))
|
||||
@extends('layouts.base')
|
||||
<div class="min-h-screen hero">
|
||||
<div class="text-center hero-content">
|
||||
<div class="">
|
||||
<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">→</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Server Error'))
|
||||
@section('code', '500')
|
||||
@section('message', __('Server Error'))
|
||||
@extends('layouts.base')
|
||||
<div class="min-h-screen hero ">
|
||||
<div class="text-center hero-content">
|
||||
<div>
|
||||
<p class="font-mono text-6xl font-semibold text-warning">500</p>
|
||||
<h1 class="mt-4 font-bold tracking-tight text-white">Something is not okay, are you okay?</h1>
|
||||
<p class="mt-6 text-base leading-7 text-neutral-300">There has been an error, we are working on it.
|
||||
</p>
|
||||
@if ($exception->getMessage() !== '')
|
||||
<p class="mt-6 text-base leading-7 text-red-500">Error: {{ $exception->getMessage() }}
|
||||
</p>
|
||||
@endif
|
||||
<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">→</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Service Unavailable'))
|
||||
@section('code', '503')
|
||||
@section('message', __('Service Unavailable'))
|
||||
@extends('layouts.base')
|
||||
<div class="min-h-screen hero ">
|
||||
<div class="text-center hero-content">
|
||||
<div>
|
||||
<p class="font-mono text-6xl font-semibold text-warning">503</p>
|
||||
<h1 class="mt-4 font-bold tracking-tight text-white">We are working on serious things.</h1>
|
||||
<p class="mt-6 text-base leading-7 text-neutral-300">Service Unavailable. Be right back. Thanks for your
|
||||
patience.
|
||||
</p>
|
||||
<div class="flex items-center justify-center mt-10 gap-x-6">
|
||||
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact
|
||||
support
|
||||
<span aria-hidden="true">→</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>@yield('title')</title>
|
||||
|
||||
<!-- Styles -->
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
background-color: #fff;
|
||||
color: #636b6f;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-weight: 100;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.full-height {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.position-ref {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36px;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="flex-center position-ref full-height">
|
||||
<div class="content">
|
||||
<div class="title">
|
||||
@yield('message')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,552 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>@yield('title')</title>
|
||||
|
||||
<style>
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
html {
|
||||
line-height: 1.15;
|
||||
-webkit-text-size-adjust: 100%
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: transparent
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
line-height: 1.5
|
||||
}
|
||||
|
||||
*,
|
||||
:after,
|
||||
:before {
|
||||
box-sizing: border-box;
|
||||
border: 0 solid #e2e8f0
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace
|
||||
}
|
||||
|
||||
svg,
|
||||
video {
|
||||
display: block;
|
||||
vertical-align: middle
|
||||
}
|
||||
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
--bg-opacity: 1;
|
||||
background-color: #fff;
|
||||
background-color: rgba(255, 255, 255, var(--bg-opacity))
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
--bg-opacity: 1;
|
||||
background-color: #f7fafc;
|
||||
background-color: rgba(247, 250, 252, var(--bg-opacity))
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
--border-opacity: 1;
|
||||
border-color: #edf2f7;
|
||||
border-color: rgba(237, 242, 247, var(--border-opacity))
|
||||
}
|
||||
|
||||
.border-gray-400 {
|
||||
--border-opacity: 1;
|
||||
border-color: #cbd5e0;
|
||||
border-color: rgba(203, 213, 224, var(--border-opacity))
|
||||
}
|
||||
|
||||
.border-t {
|
||||
border-top-width: 1px
|
||||
}
|
||||
|
||||
.border-r {
|
||||
border-right-width: 1px
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.h-5 {
|
||||
height: 1.25rem
|
||||
}
|
||||
|
||||
.h-8 {
|
||||
height: 2rem
|
||||
}
|
||||
|
||||
.h-16 {
|
||||
height: 4rem
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: .875rem
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem
|
||||
}
|
||||
|
||||
.leading-7 {
|
||||
line-height: 1.75rem
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: .25rem
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: .5rem
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: .5rem
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: .5rem
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem
|
||||
}
|
||||
|
||||
.ml-4 {
|
||||
margin-left: 1rem
|
||||
}
|
||||
|
||||
.mt-8 {
|
||||
margin-top: 2rem
|
||||
}
|
||||
|
||||
.ml-12 {
|
||||
margin-left: 3rem
|
||||
}
|
||||
|
||||
.-mt-px {
|
||||
margin-top: -1px
|
||||
}
|
||||
|
||||
.max-w-xl {
|
||||
max-width: 36rem
|
||||
}
|
||||
|
||||
.max-w-6xl {
|
||||
max-width: 72rem
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem
|
||||
}
|
||||
|
||||
.py-4 {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem
|
||||
}
|
||||
|
||||
.px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem
|
||||
}
|
||||
|
||||
.px-6 {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem
|
||||
}
|
||||
|
||||
.pt-8 {
|
||||
padding-top: 2rem
|
||||
}
|
||||
|
||||
.fixed {
|
||||
position: fixed
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative
|
||||
}
|
||||
|
||||
.top-0 {
|
||||
top: 0
|
||||
}
|
||||
|
||||
.right-0 {
|
||||
right: 0
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06)
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.text-gray-200 {
|
||||
--text-opacity: 1;
|
||||
color: #edf2f7;
|
||||
color: rgba(237, 242, 247, var(--text-opacity))
|
||||
}
|
||||
|
||||
.text-gray-300 {
|
||||
--text-opacity: 1;
|
||||
color: #e2e8f0;
|
||||
color: rgba(226, 232, 240, var(--text-opacity))
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
--text-opacity: 1;
|
||||
color: #cbd5e0;
|
||||
color: rgba(203, 213, 224, var(--text-opacity))
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
--text-opacity: 1;
|
||||
color: #a0aec0;
|
||||
color: rgba(160, 174, 192, var(--text-opacity))
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
--text-opacity: 1;
|
||||
color: #718096;
|
||||
color: rgba(113, 128, 150, var(--text-opacity))
|
||||
}
|
||||
|
||||
.text-gray-700 {
|
||||
--text-opacity: 1;
|
||||
color: #4a5568;
|
||||
color: rgba(74, 85, 104, var(--text-opacity))
|
||||
}
|
||||
|
||||
.text-gray-900 {
|
||||
--text-opacity: 1;
|
||||
color: #1a202c;
|
||||
color: rgba(26, 32, 44, var(--text-opacity))
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.antialiased {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale
|
||||
}
|
||||
|
||||
.tracking-wider {
|
||||
letter-spacing: .05em
|
||||
}
|
||||
|
||||
.w-5 {
|
||||
width: 1.25rem
|
||||
}
|
||||
|
||||
.w-8 {
|
||||
width: 2rem
|
||||
}
|
||||
|
||||
.w-auto {
|
||||
width: auto
|
||||
}
|
||||
|
||||
.grid-cols-1 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr))
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg)
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(1turn)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg)
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(1turn)
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes ping {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
75%,
|
||||
to {
|
||||
transform: scale(2);
|
||||
opacity: 0
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ping {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
75%,
|
||||
to {
|
||||
transform: scale(2);
|
||||
opacity: 0
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes pulse {
|
||||
|
||||
0%,
|
||||
to {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: .5
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
to {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: .5
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes bounce {
|
||||
|
||||
0%,
|
||||
to {
|
||||
transform: translateY(-25%);
|
||||
-webkit-animation-timing-function: cubic-bezier(.8, 0, 1, 1);
|
||||
animation-timing-function: cubic-bezier(.8, 0, 1, 1)
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(0);
|
||||
-webkit-animation-timing-function: cubic-bezier(0, 0, .2, 1);
|
||||
animation-timing-function: cubic-bezier(0, 0, .2, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
|
||||
0%,
|
||||
to {
|
||||
transform: translateY(-25%);
|
||||
-webkit-animation-timing-function: cubic-bezier(.8, 0, 1, 1);
|
||||
animation-timing-function: cubic-bezier(.8, 0, 1, 1)
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(0);
|
||||
-webkit-animation-timing-function: cubic-bezier(0, 0, .2, 1);
|
||||
animation-timing-function: cubic-bezier(0, 0, .2, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:rounded-lg {
|
||||
border-radius: .5rem
|
||||
}
|
||||
|
||||
.sm\:block {
|
||||
display: block
|
||||
}
|
||||
|
||||
.sm\:items-center {
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.sm\:justify-start {
|
||||
justify-content: flex-start
|
||||
}
|
||||
|
||||
.sm\:justify-between {
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
.sm\:h-20 {
|
||||
height: 5rem
|
||||
}
|
||||
|
||||
.sm\:ml-0 {
|
||||
margin-left: 0
|
||||
}
|
||||
|
||||
.sm\:px-6 {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem
|
||||
}
|
||||
|
||||
.sm\:pt-0 {
|
||||
padding-top: 0
|
||||
}
|
||||
|
||||
.sm\:text-left {
|
||||
text-align: left
|
||||
}
|
||||
|
||||
.sm\:text-right {
|
||||
text-align: right
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:border-t-0 {
|
||||
border-top-width: 0
|
||||
}
|
||||
|
||||
.md\:border-l {
|
||||
border-left-width: 1px
|
||||
}
|
||||
|
||||
.md\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr))
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\:px-8 {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:bg-gray-800 {
|
||||
--bg-opacity: 1;
|
||||
background-color: #2d3748;
|
||||
background-color: rgba(45, 55, 72, var(--bg-opacity))
|
||||
}
|
||||
|
||||
.dark\:bg-gray-900 {
|
||||
--bg-opacity: 1;
|
||||
background-color: #1a202c;
|
||||
background-color: rgba(26, 32, 44, var(--bg-opacity))
|
||||
}
|
||||
|
||||
.dark\:border-gray-700 {
|
||||
--border-opacity: 1;
|
||||
border-color: #4a5568;
|
||||
border-color: rgba(74, 85, 104, var(--border-opacity))
|
||||
}
|
||||
|
||||
.dark\:text-white {
|
||||
--text-opacity: 1;
|
||||
color: #fff;
|
||||
color: rgba(255, 255, 255, var(--text-opacity))
|
||||
}
|
||||
|
||||
.dark\:text-gray-400 {
|
||||
--text-opacity: 1;
|
||||
color: #cbd5e0;
|
||||
color: rgba(203, 213, 224, var(--text-opacity))
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="antialiased">
|
||||
<div
|
||||
class="relative flex justify-center min-h-screen bg-gray-100 items-top dark:bg-gray-900 sm:items-center sm:pt-0">
|
||||
<div class="max-w-xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="flex items-center pt-8 sm:justify-start sm:pt-0">
|
||||
<div class="px-4 text-lg tracking-wider text-gray-500 border-r border-gray-400">
|
||||
@yield('code')
|
||||
</div>
|
||||
|
||||
<div class="ml-4 text-lg tracking-wider text-gray-500 uppercase">
|
||||
@yield('message')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -25,6 +25,14 @@
|
||||
|
||||
<body>
|
||||
@livewireScripts
|
||||
@if (isSubscriptionActive() || isDev())
|
||||
<dialog id="help" class="modal">
|
||||
<livewire:help />
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
@endif
|
||||
<x-toaster-hub />
|
||||
<x-version class="fixed left-2 bottom-1" />
|
||||
<script>
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
<div class="stat-value">{{ $s3s }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- <x-forms.button wire:click='getIptables'>Get IPTABLES</x-forms.button> --}}
|
||||
</div>
|
||||
|
||||
10
resources/views/livewire/help.blade.php
Normal file
10
resources/views/livewire/help.blade.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="flex flex-col gap-2 rounded modal-box">
|
||||
<h3>How can we help?</h3>
|
||||
<div>You can report bug about the current page (details will be included automatically), or send us general feedback.</div>
|
||||
<form wire:submit.prevent="submit" class="flex flex-col gap-4 pt-4">
|
||||
<x-forms.input id="subject" label="Subject" placeholder="Summary of your problem."></x-forms.input>
|
||||
<x-forms.textarea id="description" label="Message"
|
||||
placeholder="Please provide as much information as possible."></x-forms.textarea>
|
||||
<x-forms.button class="w-full mt-4" type="submit">Send Request</x-forms.button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
<x-forms.button type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
@if ($model->discord_enabled)
|
||||
@if ($team->discord_enabled)
|
||||
<x-forms.button class="text-white normal-case btn btn-xs no-animation btn-primary"
|
||||
wire:click="sendTestNotification">
|
||||
Send Test Notifications
|
||||
@@ -13,27 +13,38 @@
|
||||
@endif
|
||||
</div>
|
||||
<div class="w-48">
|
||||
<x-forms.checkbox instantSave id="model.discord_enabled" label="Notification Enabled" />
|
||||
<x-forms.checkbox instantSave id="team.discord_enabled" label="Notification Enabled" />
|
||||
</div>
|
||||
<x-forms.input type="password"
|
||||
helper="Generate a webhook in Discord.<br>Example: https://discord.com/api/webhooks/...." required
|
||||
id="model.discord_webhook_url" label="Webhook" />
|
||||
id="team.discord_webhook_url" label="Webhook" />
|
||||
</form>
|
||||
@if (data_get($model, 'discord_enabled'))
|
||||
<h3 class="mt-4">Subscribe to events</h3>
|
||||
@if (data_get($team, 'discord_enabled'))
|
||||
<h2 class="mt-4">Subscribe to events</h2>
|
||||
<div class="w-64">
|
||||
|
||||
|
||||
@if (isDev())
|
||||
<x-forms.checkbox instantSave="saveModel" id="model.discord_notifications_test" label="Test" />
|
||||
<h3 class="mt-4">Test</h3>
|
||||
<div class="flex items-end gap-10">
|
||||
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_test" label="Enabled" />
|
||||
</div>
|
||||
@endif
|
||||
<h4 class="mt-4">General</h4>
|
||||
<x-forms.checkbox instantSave="saveModel" id="model.discord_notifications_status_changes"
|
||||
label="Container Status Changes" />
|
||||
<h4 class="mt-4">Applications</h4>
|
||||
<x-forms.checkbox instantSave="saveModel" id="model.discord_notifications_deployments"
|
||||
label="Deployments" />
|
||||
<h4 class="mt-4">Databases</h4>
|
||||
<x-forms.checkbox instantSave="saveModel" id="model.discord_notifications_database_backups"
|
||||
label="Backup Statuses" />
|
||||
<h3 class="mt-4">Container Status Changes</h3>
|
||||
<div class="flex items-end gap-10">
|
||||
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_status_changes"
|
||||
label="Enabled" />
|
||||
</div>
|
||||
<h3 class="mt-4">Application Deployments</h3>
|
||||
<div class="flex items-end gap-10">
|
||||
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_deployments"
|
||||
label="Enabled" />
|
||||
</div>
|
||||
<h3 class="mt-4">Backup Status</h3>
|
||||
<div class="flex items-end gap-10">
|
||||
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_database_backups"
|
||||
label="Enabled" />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
Copy from Instance Settings
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings'))
|
||||
@if (isEmailEnabled($team) && auth()->user()->isAdminFromSession())
|
||||
<x-forms.button onclick="sendTestEmail.showModal()"
|
||||
class="text-white normal-case btn btn-xs no-animation btn-primary">
|
||||
Send Test Email
|
||||
@@ -35,6 +35,11 @@
|
||||
<x-forms.checkbox instantSave="instantSaveInstance" id="team.use_instance_email_settings"
|
||||
label="Use hosted email service" />
|
||||
</div>
|
||||
@else
|
||||
<div class="pb-4 w-96">
|
||||
<x-forms.checkbox disabled id="team.use_instance_email_settings"
|
||||
label="Use hosted email service (Pro+ subscription required)" />
|
||||
</div>
|
||||
@endif
|
||||
@if (!$team->use_instance_email_settings)
|
||||
<form class="flex flex-col items-end gap-2 pb-4 xl:flex-row" wire:submit.prevent='submitFromFields'>
|
||||
@@ -89,8 +94,8 @@
|
||||
<form wire:submit.prevent='submitResend' class="flex flex-col">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col w-full gap-2 xl:flex-row">
|
||||
<x-forms.input type="password" id="team.resend_api_key" placeholder="API key"
|
||||
label="Host" />
|
||||
<x-forms.input required type="password" id="team.resend_api_key" placeholder="API key"
|
||||
label="API Key" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-4 pt-6">
|
||||
@@ -104,19 +109,27 @@
|
||||
</div>
|
||||
@endif
|
||||
@if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings'))
|
||||
<h3 class="mt-4">Subscribe to events</h3>
|
||||
<h2 class="mt-4">Subscribe to events</h2>
|
||||
<div class="w-64">
|
||||
@if (isDev())
|
||||
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_test" label="Test" />
|
||||
<h3 class="mt-4">Test</h3>
|
||||
<div class="flex items-end gap-10">
|
||||
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_test" label="Enabled" />
|
||||
</div>
|
||||
@endif
|
||||
<h4 class="mt-4">General</h4>
|
||||
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_status_changes"
|
||||
label="Container Status Changes" />
|
||||
<h4 class="mt-4">Applications</h4>
|
||||
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_deployments" label="Deployments" />
|
||||
<h4 class="mt-4">Databases</h4>
|
||||
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_database_backups"
|
||||
label="Backup Statuses" />
|
||||
<h3 class="mt-4">Container Status Changes</h3>
|
||||
<div class="flex items-end gap-10">
|
||||
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_status_changes" label="Enabled" />
|
||||
</div>
|
||||
<h3 class="mt-4">Application Deployments</h3>
|
||||
<div class="flex items-end gap-10">
|
||||
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_deployments" label="Enabled" />
|
||||
</div>
|
||||
<h3 class="mt-4">Backup Status</h3>
|
||||
<div class="flex items-end gap-10">
|
||||
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_database_backups"
|
||||
label="Enabled" />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user