mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-27 20:49:32 +00:00
Compare commits
103 Commits
v4.0.0-bet
...
v4.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4de4587ea6 | ||
|
|
8675e1d13f | ||
|
|
6ceacc68cc | ||
|
|
4aacf134b7 | ||
|
|
0605772715 | ||
|
|
3fa53556f4 | ||
|
|
76510b8971 | ||
|
|
66162966b9 | ||
|
|
71e1571c39 | ||
|
|
806b761e74 | ||
|
|
0176b38958 | ||
|
|
7a180c7310 | ||
|
|
3e1120182c | ||
|
|
8e86ce671c | ||
|
|
f75a324030 | ||
|
|
3fabff93f6 | ||
|
|
e74efc4e76 | ||
|
|
472ed0753d | ||
|
|
67538ff60c | ||
|
|
ae8bd69106 | ||
|
|
2538890b52 | ||
|
|
87dd819ae4 | ||
|
|
7ec560d4a2 | ||
|
|
6f9cd6a16b | ||
|
|
923af88336 | ||
|
|
5b6667c461 | ||
|
|
6f00740f67 | ||
|
|
248863cf16 | ||
|
|
97d48823dd | ||
|
|
5eb41e1a15 | ||
|
|
4a4837d9f5 | ||
|
|
4ad72fab7b | ||
|
|
fe68e45609 | ||
|
|
291b9a84ef | ||
|
|
2f9b7b188a | ||
|
|
d04d41bc23 | ||
|
|
6cb3d7167f | ||
|
|
90b1659a18 | ||
|
|
1aaf44f9b0 | ||
|
|
d7cfb84351 | ||
|
|
d28cf0b76d | ||
|
|
b4a3236284 | ||
|
|
556168892d | ||
|
|
77667be570 | ||
|
|
f48a912287 | ||
|
|
af30d0831d | ||
|
|
5989eb8f6e | ||
|
|
2bb778834b | ||
|
|
a5ce191e4d | ||
|
|
7617756576 | ||
|
|
0dfd3a5b0e | ||
|
|
61a54f48c5 | ||
|
|
5ca0237e34 | ||
|
|
ab1207e461 | ||
|
|
75fea4f7c0 | ||
|
|
fb34eb5394 |
@@ -6,6 +6,7 @@
|
||||
USERID=
|
||||
GROUPID=
|
||||
############################################################################################################
|
||||
APP_NAME=Coolify-localhost
|
||||
APP_ID=development
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
|
||||
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,
|
||||
]
|
||||
|
||||
@@ -9,15 +9,20 @@ class SaveConfigurationSync
|
||||
{
|
||||
public function __invoke(Server $server, string $configuration)
|
||||
{
|
||||
$proxy_path = get_proxy_path();
|
||||
$docker_compose_yml_base64 = base64_encode($configuration);
|
||||
try {
|
||||
$proxy_path = get_proxy_path();
|
||||
$docker_compose_yml_base64 = base64_encode($configuration);
|
||||
|
||||
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
|
||||
$server->save();
|
||||
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
|
||||
$server->save();
|
||||
|
||||
instant_remote_process([
|
||||
"mkdir -p $proxy_path",
|
||||
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
|
||||
], $server);
|
||||
} catch (\Throwable $th) {
|
||||
ray($th);
|
||||
}
|
||||
|
||||
instant_remote_process([
|
||||
"mkdir -p $proxy_path",
|
||||
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
|
||||
], $server);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,6 @@ class StartProxy
|
||||
{
|
||||
public function __invoke(Server $server): Activity
|
||||
{
|
||||
// TODO: check for other proxies
|
||||
if (is_null(data_get($server, 'proxy.type'))) {
|
||||
$server->proxy->type = ProxyTypes::TRAEFIK_V2->value;
|
||||
$server->proxy->status = ProxyStatus::EXITED->value;
|
||||
$server->save();
|
||||
}
|
||||
$proxy_path = get_proxy_path();
|
||||
$networks = collect($server->standaloneDockers)->map(function ($docker) {
|
||||
return $docker['network'];
|
||||
|
||||
@@ -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...",
|
||||
@@ -37,11 +43,14 @@ class InstallDocker
|
||||
"docker network create --attachable coolify",
|
||||
"echo ####### Done!"
|
||||
], $server);
|
||||
StandaloneDocker::create([
|
||||
'name' => 'coolify',
|
||||
'network' => 'coolify',
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
$found = StandaloneDocker::where('server_id', $server->id);
|
||||
if ($found->count() == 0) {
|
||||
StandaloneDocker::create([
|
||||
'name' => 'coolify',
|
||||
'network' => 'coolify',
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ use App\Models\Server;
|
||||
|
||||
class UpdateCoolify
|
||||
{
|
||||
public Server $server;
|
||||
public string $latest_version;
|
||||
public string $current_version;
|
||||
public ?Server $server = null;
|
||||
public ?string $latestVersion = null;
|
||||
public ?string $currentVersion = null;
|
||||
|
||||
public function __invoke(bool $force)
|
||||
{
|
||||
@@ -17,13 +17,16 @@ class UpdateCoolify
|
||||
$settings = InstanceSettings::get();
|
||||
ray('Running InstanceAutoUpdateJob');
|
||||
$localhost_name = 'localhost';
|
||||
$this->server = Server::where('name', $localhost_name)->firstOrFail();
|
||||
$this->latest_version = get_latest_version_of_coolify();
|
||||
$this->current_version = config('version');
|
||||
ray('latest version:' . $this->latest_version . " current version: " . $this->current_version . ' force: ' . $force);
|
||||
$this->server = Server::where('name', $localhost_name)->first();
|
||||
if (!$this->server) {
|
||||
return;
|
||||
}
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
$this->currentVersion = config('version');
|
||||
ray('latest version:' . $this->latestVersion . " current version: " . $this->currentVersion . ' force: ' . $force);
|
||||
if ($settings->next_channel) {
|
||||
ray('next channel enabled');
|
||||
$this->latest_version = 'next';
|
||||
$this->latestVersion = 'next';
|
||||
}
|
||||
if ($force) {
|
||||
$this->update();
|
||||
@@ -31,15 +34,15 @@ class UpdateCoolify
|
||||
if (!$settings->is_auto_update_enabled) {
|
||||
return 'Auto update is disabled';
|
||||
}
|
||||
if ($this->latest_version === $this->current_version) {
|
||||
if ($this->latestVersion === $this->currentVersion) {
|
||||
return 'Already on latest version';
|
||||
}
|
||||
if (version_compare($this->latest_version, $this->current_version, '<')) {
|
||||
if (version_compare($this->latestVersion, $this->currentVersion, '<')) {
|
||||
return 'Latest version is lower than current version?!';
|
||||
}
|
||||
$this->update();
|
||||
}
|
||||
send_internal_notification('InstanceAutoUpdateJob done to version: ' . $this->latest_version . ' from version: ' . $this->current_version);
|
||||
send_internal_notification('InstanceAutoUpdateJob done to version: ' . $this->latestVersion . ' from version: ' . $this->currentVersion);
|
||||
} catch (\Exception $th) {
|
||||
ray('InstanceAutoUpdateJob failed');
|
||||
ray($th->getMessage());
|
||||
@@ -51,7 +54,7 @@ class UpdateCoolify
|
||||
private function update()
|
||||
{
|
||||
if (isDev()) {
|
||||
ray("Running update on local docker container. Updating to $this->latest_version");
|
||||
ray("Running update on local docker container. Updating to $this->latestVersion");
|
||||
remote_process([
|
||||
"sleep 10"
|
||||
], $this->server);
|
||||
@@ -61,7 +64,7 @@ class UpdateCoolify
|
||||
ray('Running update on production server');
|
||||
remote_process([
|
||||
"curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh",
|
||||
"bash /data/coolify/source/upgrade.sh $this->latest_version"
|
||||
"bash /data/coolify/source/upgrade.sh $this->latestVersion"
|
||||
], $this->server);
|
||||
return;
|
||||
}
|
||||
|
||||
184
app/Console/Commands/TestEmail.php
Normal file
184
app/Console/Commands/TestEmail.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\TeamInvitation;
|
||||
use App\Models\User;
|
||||
use App\Notifications\Application\DeploymentFailed;
|
||||
use App\Notifications\Application\DeploymentSuccess;
|
||||
use App\Notifications\Application\StatusChanged;
|
||||
use App\Notifications\Database\BackupFailed;
|
||||
use App\Notifications\Database\BackupSuccess;
|
||||
use App\Notifications\Test;
|
||||
use App\Notifications\TransactionalEmails\InvitationLink;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Mail\Message;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Mail;
|
||||
use Str;
|
||||
|
||||
use function Laravel\Prompts\select;
|
||||
|
||||
class TestEmail extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'email:test';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Send a test email to the admin';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
private ?MailMessage $mail = null;
|
||||
public function handle()
|
||||
{
|
||||
$email = select(
|
||||
'Which Email should be sent?',
|
||||
options: [
|
||||
'emails-test' => 'Test',
|
||||
'application-deployment-success' => 'Application - Deployment Success',
|
||||
'application-deployment-failed' => 'Application - Deployment Failed',
|
||||
'application-status-changed' => 'Application - Status Changed',
|
||||
'backup-success' => 'Database - Backup Success',
|
||||
'backup-failed' => 'Database - Backup Failed',
|
||||
'invitation-link' => 'Invitation Link',
|
||||
'waitlist-invitation-link' => 'Waitlist Invitation Link',
|
||||
'waitlist-confirmation' => 'Waitlist Confirmation',
|
||||
],
|
||||
);
|
||||
$type = set_transanctional_email_settings();
|
||||
if (!$type) {
|
||||
throw new Exception('No email settings found.');
|
||||
}
|
||||
$this->mail = new MailMessage();
|
||||
$this->mail->subject("Test Email");
|
||||
switch ($email) {
|
||||
case 'emails-test':
|
||||
$this->mail = (new Test())->toMail();
|
||||
break;
|
||||
case 'application-deployment-success':
|
||||
$application = Application::all()->first();
|
||||
$this->mail = (new DeploymentSuccess($application, 'test'))->toMail();
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'application-deployment-failed':
|
||||
$application = Application::all()->first();
|
||||
$preview = ApplicationPreview::all()->first();
|
||||
if (!$preview) {
|
||||
$preview = ApplicationPreview::create([
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => 1,
|
||||
'pull_request_html_url' => 'http://example.com',
|
||||
'fqdn' => $application->fqdn,
|
||||
]);
|
||||
}
|
||||
$this->mail = (new DeploymentFailed($application, 'test'))->toMail();
|
||||
$this->sendEmail();
|
||||
$this->mail = (new DeploymentFailed($application, 'test', $preview))->toMail();
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'application-status-changed':
|
||||
$application = Application::all()->first();
|
||||
$this->mail = (new StatusChanged($application))->toMail();
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'backup-failed':
|
||||
$backup = ScheduledDatabaseBackup::all()->first();
|
||||
$db = StandalonePostgresql::all()->first();
|
||||
if (!$backup) {
|
||||
$backup = ScheduledDatabaseBackup::create([
|
||||
'enabled' => true,
|
||||
'frequency' => 'daily',
|
||||
'save_s3' => false,
|
||||
'database_id' => $db->id,
|
||||
'database_type' => $db->getMorphClass(),
|
||||
'team_id' => 0,
|
||||
]);
|
||||
}
|
||||
$output = 'Because of an error, the backup of the database ' . $db->name . ' failed.';
|
||||
$this->mail = (new BackupFailed($backup, $db, $output))->toMail();
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'backup-success':
|
||||
$backup = ScheduledDatabaseBackup::all()->first();
|
||||
$db = StandalonePostgresql::all()->first();
|
||||
if (!$backup) {
|
||||
$backup = ScheduledDatabaseBackup::create([
|
||||
'enabled' => true,
|
||||
'frequency' => 'daily',
|
||||
'save_s3' => false,
|
||||
'database_id' => $db->id,
|
||||
'database_type' => $db->getMorphClass(),
|
||||
'team_id' => 0,
|
||||
]);
|
||||
}
|
||||
$this->mail = (new BackupSuccess($backup, $db))->toMail();
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'invitation-link':
|
||||
$user = User::all()->first();
|
||||
$invitation = TeamInvitation::whereEmail($user->email)->first();
|
||||
if (!$invitation) {
|
||||
$invitation = TeamInvitation::create([
|
||||
'uuid' => Str::uuid(),
|
||||
'email' => $user->email,
|
||||
'team_id' => 1,
|
||||
'link' => 'http://example.com',
|
||||
]);
|
||||
}
|
||||
$this->mail = (new InvitationLink($user))->toMail();
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'waitlist-invitation-link':
|
||||
$this->mail = new MailMessage();
|
||||
$this->mail->view('emails.waitlist-invitation', [
|
||||
'email' => 'test2@example.com',
|
||||
'password' => "supersecretpassword",
|
||||
]);
|
||||
$this->mail->subject('Congratulations! You are invited to join Coolify Cloud.');
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'waitlist-confirmation':
|
||||
$this->mail = new MailMessage();
|
||||
$this->mail->view(
|
||||
'emails.waitlist-confirmation',
|
||||
[
|
||||
'confirmation_url' => 'http://example.com',
|
||||
'cancel_url' => 'http://example.com',
|
||||
]
|
||||
);
|
||||
$this->mail->subject('You are on the waitlist!');
|
||||
$this->sendEmail();
|
||||
break;
|
||||
}
|
||||
}
|
||||
private function sendEmail()
|
||||
{
|
||||
Mail::send(
|
||||
[],
|
||||
[],
|
||||
fn (Message $message) => $message
|
||||
->from(
|
||||
'internal@example.com',
|
||||
'Test Email',
|
||||
)
|
||||
->to('test@example.com')
|
||||
->subject($this->mail->subject)
|
||||
->html((string)$this->mail->render())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
$schedule->job(new DockerCleanupJob)->everyOddHour();
|
||||
} else {
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute();
|
||||
$schedule->job(new ResourceStatusJob)->everyMinute();
|
||||
$schedule->job(new CheckResaleLicenseJob)->hourly();
|
||||
$schedule->job(new ProxyCheckJob)->everyFiveMinutes();
|
||||
$schedule->job(new DockerCleanupJob)->everyTenMinutes();
|
||||
$schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes();
|
||||
$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();
|
||||
}
|
||||
$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';
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Exceptions;
|
||||
use App\Models\InstanceSettings;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Sentry\Laravel\Integration;
|
||||
use Sentry\State\Scope;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
@@ -48,6 +49,11 @@ class Handler extends ExceptionHandler
|
||||
if ($this->settings->do_not_track || isDev()) {
|
||||
return;
|
||||
}
|
||||
app('sentry')->configureScope(
|
||||
function (Scope $scope){
|
||||
$scope->setUser(['id'=> config('sentry.server_name')]);
|
||||
}
|
||||
);
|
||||
Integration::captureUnhandledException($e);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,39 +5,57 @@ namespace App\Http\Controllers;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\TeamInvitation;
|
||||
use App\Models\User;
|
||||
use App\Models\Waitlist;
|
||||
use 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 waitlist() {
|
||||
$waiting_in_line = Waitlist::whereVerified(true)->count();
|
||||
return view('auth.waitlist', [
|
||||
'waiting_in_line' => $waiting_in_line,
|
||||
]);
|
||||
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 (!is_cloud()) {
|
||||
if (!isCloud()) {
|
||||
abort(404);
|
||||
}
|
||||
return view('subscription.show', [
|
||||
return view('subscription.index', [
|
||||
'settings' => InstanceSettings::get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function license()
|
||||
{
|
||||
if (!is_cloud()) {
|
||||
if (!isCloud()) {
|
||||
abort(404);
|
||||
}
|
||||
return view('settings.license', [
|
||||
@@ -45,27 +63,12 @@ class Controller extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
public function force_passoword_reset() {
|
||||
public function force_passoword_reset()
|
||||
{
|
||||
return view('auth.force-password-reset');
|
||||
}
|
||||
public function dashboard()
|
||||
public function boarding()
|
||||
{
|
||||
$projects = Project::ownedByCurrentTeam()->get();
|
||||
$servers = Server::ownedByCurrentTeam()->get();
|
||||
$s3s = S3Storage::ownedByCurrentTeam()->get();
|
||||
$resources = 0;
|
||||
foreach ($projects as $project) {
|
||||
$resources += $project->applications->count();
|
||||
$resources += $project->postgresqls->count();
|
||||
}
|
||||
return view('dashboard', [
|
||||
'servers' => $servers->count(),
|
||||
'projects' => $projects->count(),
|
||||
'resources' => $resources,
|
||||
's3s' => $s3s,
|
||||
]);
|
||||
}
|
||||
public function boarding() {
|
||||
if (currentTeam()->boarding || isDev()) {
|
||||
return view('boarding');
|
||||
} else {
|
||||
@@ -97,7 +100,7 @@ class Controller extends BaseController
|
||||
if (auth()->user()->isAdminFromSession()) {
|
||||
$invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get();
|
||||
}
|
||||
return view('team.show', [
|
||||
return view('team.index', [
|
||||
'invitations' => $invitations,
|
||||
]);
|
||||
}
|
||||
@@ -146,7 +149,7 @@ class Controller extends BaseController
|
||||
if ($diff <= config('constants.invitation.link.expiration')) {
|
||||
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
|
||||
$invitation->delete();
|
||||
return redirect()->route('team.show');
|
||||
return redirect()->route('team.index');
|
||||
} else {
|
||||
$invitation->delete();
|
||||
abort(401);
|
||||
@@ -168,7 +171,7 @@ class Controller extends BaseController
|
||||
abort(401);
|
||||
}
|
||||
$invitation->delete();
|
||||
return redirect()->route('team.show');
|
||||
return redirect()->route('team.index');
|
||||
} catch (Throwable $th) {
|
||||
throw $th;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ class ProjectController extends Controller
|
||||
{
|
||||
$type = request()->query('type');
|
||||
$destination_uuid = request()->query('destination');
|
||||
$server = requesT()->query('server');
|
||||
|
||||
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
if (!$project) {
|
||||
|
||||
@@ -12,20 +12,21 @@ class ServerController extends Controller
|
||||
|
||||
public function new_server()
|
||||
{
|
||||
if (!is_cloud() || isInstanceAdmin()) {
|
||||
$privateKeys = PrivateKey::ownedByCurrentTeam()->get();
|
||||
if (!isCloud()) {
|
||||
return view('server.create', [
|
||||
'limit_reached' => false,
|
||||
'private_keys' => PrivateKey::ownedByCurrentTeam()->get(),
|
||||
'private_keys' => $privateKeys,
|
||||
]);
|
||||
}
|
||||
$servers = currentTeam()->servers->count();
|
||||
$subscription = currentTeam()?->subscription->type();
|
||||
$your_limit = config('constants.limits.server')[strtolower($subscription)];
|
||||
$limit_reached = $servers >= $your_limit;
|
||||
$team = currentTeam();
|
||||
$servers = $team->servers->count();
|
||||
['serverLimit' => $serverLimit] = $team->limits;
|
||||
$limit_reached = $servers >= $serverLimit;
|
||||
|
||||
return view('server.create', [
|
||||
'limit_reached' => $limit_reached,
|
||||
'private_keys' => PrivateKey::ownedByCurrentTeam()->get(),
|
||||
'private_keys' => $privateKeys,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class Kernel extends HttpKernel
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\CheckForcePasswordReset::class,
|
||||
\App\Http\Middleware\SubscriptionValid::class,
|
||||
\App\Http\Middleware\IsSubscriptionValid::class,
|
||||
\App\Http\Middleware\IsBoardingFlow::class,
|
||||
|
||||
],
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire;
|
||||
namespace App\Http\Livewire\Boarding;
|
||||
|
||||
use App\Actions\Server\InstallDocker;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
class Boarding extends Component
|
||||
class Index extends Component
|
||||
{
|
||||
public string $currentState = 'welcome';
|
||||
|
||||
public ?Collection $privateKeys = null;
|
||||
public ?int $selectedExistingPrivateKey = null;
|
||||
public ?string $privateKeyType = null;
|
||||
public ?string $privateKey = null;
|
||||
public ?string $publicKey = null;
|
||||
@@ -19,6 +22,8 @@ class Boarding extends Component
|
||||
public ?string $privateKeyDescription = null;
|
||||
public ?PrivateKey $createdPrivateKey = null;
|
||||
|
||||
public ?Collection $servers = null;
|
||||
public ?int $selectedExistingServer = null;
|
||||
public ?string $remoteServerName = null;
|
||||
public ?string $remoteServerDescription = null;
|
||||
public ?string $remoteServerHost = null;
|
||||
@@ -26,6 +31,8 @@ class Boarding extends Component
|
||||
public ?string $remoteServerUser = 'root';
|
||||
public ?Server $createdServer = null;
|
||||
|
||||
public Collection|array $projects = [];
|
||||
public ?int $selectedExistingProject = null;
|
||||
public ?Project $createdProject = null;
|
||||
|
||||
public function mount()
|
||||
@@ -45,6 +52,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
$this->remoteServerHost = 'coolify-testing-host';
|
||||
}
|
||||
}
|
||||
public function welcome() {
|
||||
if (isCloud()) {
|
||||
return $this->setServerType('remote');
|
||||
}
|
||||
$this->currentState = 'select-server-type';
|
||||
}
|
||||
public function restartBoarding()
|
||||
{
|
||||
if ($this->createdServer) {
|
||||
@@ -63,20 +76,62 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
refreshSession();
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
public function setServer(string $type)
|
||||
|
||||
public function setServerType(string $type)
|
||||
{
|
||||
if ($type === 'localhost') {
|
||||
$this->createdServer = Server::find(0);
|
||||
if (!$this->createdServer) {
|
||||
return $this->emit('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
|
||||
}
|
||||
$this->currentState = 'select-proxy';
|
||||
return $this->validateServer();
|
||||
} elseif ($type === 'remote') {
|
||||
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
|
||||
if ($this->privateKeys->count() > 0) {
|
||||
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
|
||||
}
|
||||
$this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
|
||||
if ($this->servers->count() > 0) {
|
||||
$this->selectedExistingServer = $this->servers->first()->id;
|
||||
$this->currentState = 'select-existing-server';
|
||||
return;
|
||||
}
|
||||
$this->currentState = 'private-key';
|
||||
}
|
||||
}
|
||||
public function selectExistingServer()
|
||||
{
|
||||
$this->createdServer = Server::find($this->selectedExistingServer);
|
||||
if (!$this->createdServer) {
|
||||
$this->emit('error', 'Server is not found.');
|
||||
$this->currentState = 'private-key';
|
||||
return;
|
||||
}
|
||||
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
|
||||
$this->validateServer();
|
||||
$this->getProxyType();
|
||||
$this->getProjects();
|
||||
}
|
||||
public function getProxyType() {
|
||||
$proxyTypeSet = $this->createdServer->proxy->type;
|
||||
if (!$proxyTypeSet) {
|
||||
$this->currentState = 'select-proxy';
|
||||
return;
|
||||
}
|
||||
$this->getProjects();
|
||||
}
|
||||
public function selectExistingPrivateKey()
|
||||
{
|
||||
$this->currentState = 'create-server';
|
||||
}
|
||||
public function createNewServer()
|
||||
{
|
||||
$this->selectedExistingServer = null;
|
||||
$this->currentState = 'private-key';
|
||||
}
|
||||
public function setPrivateKey(string $type)
|
||||
{
|
||||
$this->selectedExistingPrivateKey = null;
|
||||
$this->privateKeyType = $type;
|
||||
if ($type === 'create' && !isDev()) {
|
||||
$this->createNewPrivateKey();
|
||||
@@ -115,11 +170,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
'private_key_id' => $this->createdPrivateKey->id,
|
||||
'team_id' => currentTeam()->id
|
||||
]);
|
||||
$this->validateServer();
|
||||
}
|
||||
public function validateServer() {
|
||||
try {
|
||||
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->createdServer);
|
||||
if (!$uptime) {
|
||||
$this->createdServer->delete();
|
||||
$this->createdPrivateKey->delete();
|
||||
throw new \Exception('Server is not reachable.');
|
||||
} else {
|
||||
$this->createdServer->settings->update([
|
||||
@@ -127,11 +183,14 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
]);
|
||||
$this->emit('success', 'Server is reachable.');
|
||||
}
|
||||
if ($dockerVersion) {
|
||||
ray($dockerVersion, $uptime);
|
||||
if (!$dockerVersion) {
|
||||
$this->emit('error', 'Docker is not installed on the server.');
|
||||
$this->currentState = 'install-docker';
|
||||
return;
|
||||
}
|
||||
$this->getProxyType();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
|
||||
}
|
||||
@@ -145,13 +204,25 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
public function selectProxy(string|null $proxyType = null)
|
||||
{
|
||||
if (!$proxyType) {
|
||||
return $this->currentState = 'create-project';
|
||||
return $this->getProjects();
|
||||
}
|
||||
$this->createdServer->proxy->type = $proxyType;
|
||||
$this->createdServer->proxy->status = 'exited';
|
||||
$this->createdServer->save();
|
||||
$this->getProjects();
|
||||
}
|
||||
|
||||
public function getProjects() {
|
||||
$this->projects = Project::ownedByCurrentTeam(['name'])->get();
|
||||
if ($this->projects->count() > 0) {
|
||||
$this->selectedExistingProject = $this->projects->first()->id;
|
||||
}
|
||||
$this->currentState = 'create-project';
|
||||
}
|
||||
public function selectExistingProject() {
|
||||
$this->createdProject = Project::find($this->selectedExistingProject);
|
||||
$this->currentState = 'create-resource';
|
||||
}
|
||||
public function createNewProject()
|
||||
{
|
||||
$this->createdProject = Project::create([
|
||||
@@ -168,7 +239,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
[
|
||||
'project_uuid' => $this->createdProject->uuid,
|
||||
'environment_name' => 'production',
|
||||
|
||||
'server'=> $this->createdServer->id,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -176,6 +247,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
{
|
||||
$this->privateKeyName = generate_random_name();
|
||||
$this->privateKeyDescription = 'Created by Coolify';
|
||||
['private' => $this->privateKey, 'public'=> $this->publicKey] = generateSSHKey();
|
||||
['private' => $this->privateKey, 'public' => $this->publicKey] = generateSSHKey();
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.boarding.index')->layout('layouts.boarding');
|
||||
}
|
||||
}
|
||||
41
app/Http/Livewire/Dashboard.php
Normal file
41
app/Http/Livewire/Dashboard.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Dashboard extends Component
|
||||
{
|
||||
public int $projects = 0;
|
||||
public int $servers = 0;
|
||||
public int $s3s = 0;
|
||||
public int $resources = 0;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->servers = Server::ownedByCurrentTeam()->get()->count();
|
||||
$this->s3s = S3Storage::ownedByCurrentTeam()->get()->count();
|
||||
$projects = Project::ownedByCurrentTeam()->get();
|
||||
foreach ($projects as $project) {
|
||||
$this->resources += $project->applications->count();
|
||||
$this->resources += $project->postgresqls->count();
|
||||
}
|
||||
$this->projects = $projects->count();
|
||||
}
|
||||
// public function getIptables()
|
||||
// {
|
||||
// $servers = Server::ownedByCurrentTeam()->get();
|
||||
// foreach ($servers as $server) {
|
||||
// checkRequiredCommands($server);
|
||||
// $iptables = instant_remote_process(['docker run --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c "iptables -L -n | jc --iptables"'], $server);
|
||||
// ray($iptables);
|
||||
// }
|
||||
// }
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.dashboard');
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Dev;
|
||||
|
||||
use App\Models\S3Storage;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class S3Test extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public $s3;
|
||||
public $file;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->s3 = S3Storage::first();
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
try {
|
||||
$this->validate([
|
||||
'file' => 'required|max:150', // 1MB Max
|
||||
]);
|
||||
set_s3_target($this->s3);
|
||||
$this->file->storeAs('files', $this->file->getClientOriginalName(), 'custom-s3');
|
||||
$this->emit('success', 'File uploaded successfully.');
|
||||
} catch (\Throwable $th) {
|
||||
return general_error_handler($th, $this, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function get_files()
|
||||
{
|
||||
set_s3_target($this->s3);
|
||||
dd(Storage::disk('custom-s3')->files('files'));
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,55 +6,151 @@ use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Notifications\Test;
|
||||
use Livewire\Component;
|
||||
use Log;
|
||||
|
||||
class EmailSettings extends Component
|
||||
{
|
||||
public Team $model;
|
||||
public Team $team;
|
||||
public string $emails;
|
||||
public bool $sharedEmailEnabled = false;
|
||||
|
||||
protected $rules = [
|
||||
'model.smtp_enabled' => 'nullable|boolean',
|
||||
'model.smtp_from_address' => 'required|email',
|
||||
'model.smtp_from_name' => 'required',
|
||||
'model.smtp_recipients' => 'nullable',
|
||||
'model.smtp_host' => 'required',
|
||||
'model.smtp_port' => 'required',
|
||||
'model.smtp_encryption' => 'nullable',
|
||||
'model.smtp_username' => 'nullable',
|
||||
'model.smtp_password' => 'nullable',
|
||||
'model.smtp_timeout' => 'nullable',
|
||||
'model.smtp_notifications_test' => 'nullable|boolean',
|
||||
'model.smtp_notifications_deployments' => 'nullable|boolean',
|
||||
'model.smtp_notifications_status_changes' => 'nullable|boolean',
|
||||
'model.smtp_notifications_database_backups' => 'nullable|boolean',
|
||||
'team.smtp_enabled' => 'nullable|boolean',
|
||||
'team.smtp_from_address' => 'required|email',
|
||||
'team.smtp_from_name' => 'required',
|
||||
'team.smtp_recipients' => 'nullable',
|
||||
'team.smtp_host' => 'required',
|
||||
'team.smtp_port' => 'required',
|
||||
'team.smtp_encryption' => 'nullable',
|
||||
'team.smtp_username' => 'nullable',
|
||||
'team.smtp_password' => 'nullable',
|
||||
'team.smtp_timeout' => 'nullable',
|
||||
'team.smtp_notifications_test' => 'nullable|boolean',
|
||||
'team.smtp_notifications_deployments' => 'nullable|boolean',
|
||||
'team.smtp_notifications_status_changes' => 'nullable|boolean',
|
||||
'team.smtp_notifications_database_backups' => 'nullable|boolean',
|
||||
'team.use_instance_email_settings' => 'boolean',
|
||||
'team.resend_enabled' => 'nullable|boolean',
|
||||
'team.resend_api_key' => 'nullable',
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
'model.smtp_from_address' => 'From Address',
|
||||
'model.smtp_from_name' => 'From Name',
|
||||
'model.smtp_recipients' => 'Recipients',
|
||||
'model.smtp_host' => 'Host',
|
||||
'model.smtp_port' => 'Port',
|
||||
'model.smtp_encryption' => 'Encryption',
|
||||
'model.smtp_username' => 'Username',
|
||||
'model.smtp_password' => 'Password',
|
||||
'team.smtp_from_address' => 'From Address',
|
||||
'team.smtp_from_name' => 'From Name',
|
||||
'team.smtp_recipients' => 'Recipients',
|
||||
'team.smtp_host' => 'Host',
|
||||
'team.smtp_port' => 'Port',
|
||||
'team.smtp_encryption' => 'Encryption',
|
||||
'team.smtp_username' => 'Username',
|
||||
'team.smtp_password' => 'Password',
|
||||
'team.smtp_timeout' => 'Timeout',
|
||||
'team.resend_enabled' => 'Resend Enabled',
|
||||
'team.resend_api_key' => 'Resend API Key',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->decrypt();
|
||||
$this->team = auth()->user()->currentTeam();
|
||||
['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits;
|
||||
$this->emails = auth()->user()->email;
|
||||
}
|
||||
|
||||
private function decrypt()
|
||||
public function submitFromFields()
|
||||
{
|
||||
if (data_get($this->model, 'smtp_password')) {
|
||||
try {
|
||||
$this->model->smtp_password = decrypt($this->model->smtp_password);
|
||||
} catch (\Exception $e) {
|
||||
try {
|
||||
$this->resetErrorBag();
|
||||
$this->validate([
|
||||
'team.smtp_from_address' => 'required|email',
|
||||
'team.smtp_from_name' => 'required',
|
||||
]);
|
||||
$this->team->save();
|
||||
$this->emit('success', 'Settings saved successfully.');
|
||||
} catch (\Exception $e) {
|
||||
return general_error_handler($e, $this);
|
||||
}
|
||||
}
|
||||
public function sendTestNotification()
|
||||
{
|
||||
$this->team->notify(new Test($this->emails));
|
||||
$this->emit('success', 'Test Email sent successfully.');
|
||||
}
|
||||
public function instantSaveInstance()
|
||||
{
|
||||
try {
|
||||
if (!$this->sharedEmailEnabled) {
|
||||
throw new \Exception('Not allowed to change settings. Please upgrade your subscription.');
|
||||
}
|
||||
$this->team->smtp_enabled = false;
|
||||
$this->team->resend_enabled = false;
|
||||
$this->team->save();
|
||||
$this->emit('success', 'Settings saved successfully.');
|
||||
} catch (\Exception $e) {
|
||||
return general_error_handler($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveResend()
|
||||
{
|
||||
try {
|
||||
$this->team->smtp_enabled = false;
|
||||
$this->submitResend();
|
||||
} catch (\Exception $e) {
|
||||
$this->team->smtp_enabled = false;
|
||||
return general_error_handler($e, $this);
|
||||
}
|
||||
}
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->team->resend_enabled = false;
|
||||
$this->submit();
|
||||
} catch (\Exception $e) {
|
||||
$this->team->smtp_enabled = false;
|
||||
return general_error_handler($e, $this);
|
||||
}
|
||||
}
|
||||
public function saveModel()
|
||||
{
|
||||
$this->team->save();
|
||||
if (is_a($this->team, Team::class)) {
|
||||
refreshSession();
|
||||
}
|
||||
$this->emit('success', 'Settings saved.');
|
||||
}
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->resetErrorBag();
|
||||
$this->validate([
|
||||
'team.smtp_from_address' => 'required|email',
|
||||
'team.smtp_from_name' => 'required',
|
||||
'team.smtp_host' => 'required',
|
||||
'team.smtp_port' => 'required|numeric',
|
||||
'team.smtp_encryption' => 'nullable',
|
||||
'team.smtp_username' => 'nullable',
|
||||
'team.smtp_password' => 'nullable',
|
||||
'team.smtp_timeout' => 'nullable',
|
||||
]);
|
||||
$this->team->save();
|
||||
$this->emit('success', 'Settings saved successfully.');
|
||||
} catch (\Exception $e) {
|
||||
$this->team->smtp_enabled = false;
|
||||
return general_error_handler($e, $this);
|
||||
}
|
||||
}
|
||||
public function submitResend()
|
||||
{
|
||||
try {
|
||||
$this->resetErrorBag();
|
||||
$this->validate([
|
||||
'team.resend_api_key' => 'required'
|
||||
]);
|
||||
$this->team->save();
|
||||
refreshSession();
|
||||
$this->emit('success', 'Settings saved successfully.');
|
||||
} catch (\Exception $e) {
|
||||
$this->team->resend_enabled = false;
|
||||
return general_error_handler($e, $this);
|
||||
}
|
||||
}
|
||||
public function copyFromInstanceSettings()
|
||||
{
|
||||
$settings = InstanceSettings::get();
|
||||
@@ -72,55 +168,22 @@ class EmailSettings extends Component
|
||||
'smtp_password' => $settings->smtp_password,
|
||||
'smtp_timeout' => $settings->smtp_timeout,
|
||||
]);
|
||||
$this->decrypt();
|
||||
if (is_a($team, Team::class)) {
|
||||
refreshSession();
|
||||
}
|
||||
$this->model = $team;
|
||||
$this->emit('success', 'Settings saved.');
|
||||
} else {
|
||||
$this->emit('error', 'Instance SMTP settings are not enabled.');
|
||||
}
|
||||
}
|
||||
|
||||
public function sendTestNotification()
|
||||
{
|
||||
$this->model->notify(new Test($this->emails));
|
||||
$this->emit('success', 'Test Email sent successfully.');
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->submit();
|
||||
} catch (\Exception $e) {
|
||||
$this->model->smtp_enabled = false;
|
||||
$this->validate();
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->resetErrorBag();
|
||||
$this->validate();
|
||||
|
||||
if ($this->model->smtp_password) {
|
||||
$this->model->smtp_password = encrypt($this->model->smtp_password);
|
||||
} else {
|
||||
$this->model->smtp_password = null;
|
||||
}
|
||||
|
||||
$this->model->smtp_recipients = str_replace(' ', '', $this->model->smtp_recipients);
|
||||
$this->saveModel();
|
||||
}
|
||||
|
||||
public function saveModel()
|
||||
{
|
||||
$this->model->save();
|
||||
$this->decrypt();
|
||||
if (is_a($this->model, Team::class)) {
|
||||
refreshSession();
|
||||
$this->team = $team;
|
||||
$this->emit('success', 'Settings saved.');
|
||||
return;
|
||||
}
|
||||
$this->emit('success', 'Settings saved.');
|
||||
if ($settings->resend_enabled) {
|
||||
$team = currentTeam();
|
||||
$team->update([
|
||||
'resend_enabled' => $settings->resend_enabled,
|
||||
'resend_api_key' => $settings->resend_api_key,
|
||||
]);
|
||||
refreshSession();
|
||||
$this->team = $team;
|
||||
$this->emit('success', 'Settings saved.');
|
||||
return;
|
||||
}
|
||||
$this->emit('error', 'Instance SMTP/Resend settings are not enabled.');
|
||||
}
|
||||
}
|
||||
|
||||
62
app/Http/Livewire/Notifications/TelegramSettings.php
Normal file
62
app/Http/Livewire/Notifications/TelegramSettings.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Notifications;
|
||||
|
||||
use App\Models\Team;
|
||||
use App\Notifications\Test;
|
||||
use Livewire\Component;
|
||||
|
||||
class TelegramSettings extends Component
|
||||
{
|
||||
public Team $team;
|
||||
protected $rules = [
|
||||
'team.telegram_enabled' => 'nullable|boolean',
|
||||
'team.telegram_token' => 'required|string',
|
||||
'team.telegram_chat_id' => 'required|string',
|
||||
'team.telegram_notifications_test' => 'nullable|boolean',
|
||||
'team.telegram_notifications_deployments' => 'nullable|boolean',
|
||||
'team.telegram_notifications_status_changes' => 'nullable|boolean',
|
||||
'team.telegram_notifications_database_backups' => 'nullable|boolean',
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
'team.telegram_token' => 'Token',
|
||||
'team.telegram_chat_id' => 'Chat ID',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->team = auth()->user()->currentTeam();
|
||||
}
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->submit();
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
$this->team->telegram_enabled = false;
|
||||
$this->validate();
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->resetErrorBag();
|
||||
$this->validate();
|
||||
$this->saveModel();
|
||||
}
|
||||
|
||||
public function saveModel()
|
||||
{
|
||||
$this->team->save();
|
||||
if (is_a($this->team, Team::class)) {
|
||||
refreshSession();
|
||||
}
|
||||
$this->emit('success', 'Settings saved.');
|
||||
}
|
||||
|
||||
public function sendTestNotification()
|
||||
{
|
||||
$this->team->notify(new Test());
|
||||
$this->emit('success', 'Test notification sent.');
|
||||
}
|
||||
}
|
||||
@@ -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,43 @@ 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) {
|
||||
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']);
|
||||
|
||||
@@ -7,15 +7,19 @@ use App\Models\GithubApp;
|
||||
use App\Models\Project;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use App\Traits\SaveFromRedirect;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Component;
|
||||
use Route;
|
||||
|
||||
class GithubPrivateRepository extends Component
|
||||
{
|
||||
use SaveFromRedirect;
|
||||
public $current_step = 'github_apps';
|
||||
public $github_apps;
|
||||
public GithubApp $github_app;
|
||||
public $parameters;
|
||||
public $currentRoute;
|
||||
public $query;
|
||||
public $type;
|
||||
|
||||
@@ -36,14 +40,30 @@ class GithubPrivateRepository extends Component
|
||||
public string|null $publish_directory = null;
|
||||
protected int $page = 1;
|
||||
|
||||
// public function saveFromRedirect(string $route, ?Collection $parameters = null){
|
||||
// session()->forget('from');
|
||||
// if (!$parameters || $parameters->count() === 0) {
|
||||
// $parameters = $this->parameters;
|
||||
// }
|
||||
// $parameters = collect($parameters) ?? collect([]);
|
||||
// $queries = collect($this->query) ?? collect([]);
|
||||
// $parameters = $parameters->merge($queries);
|
||||
// session(['from'=> [
|
||||
// 'back'=> $this->currentRoute,
|
||||
// 'route' => $route,
|
||||
// 'parameters' => $parameters
|
||||
// ]]);
|
||||
// return redirect()->route($route);
|
||||
// }
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->currentRoute = Route::currentRouteName();
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->query = request()->query();
|
||||
$this->repositories = $this->branches = collect();
|
||||
$this->github_apps = GithubApp::private();
|
||||
}
|
||||
|
||||
public function loadRepositories($github_app_id)
|
||||
{
|
||||
$this->repositories = collect();
|
||||
|
||||
@@ -39,7 +39,7 @@ class PublicGitRepository extends Component
|
||||
'publish_directory' => 'publish directory',
|
||||
];
|
||||
private object $repository_url_parsed;
|
||||
private GithubApp|GitlabApp $git_source;
|
||||
private GithubApp|GitlabApp|null $git_source = null;
|
||||
private string $git_host;
|
||||
private string $git_repository;
|
||||
|
||||
@@ -67,18 +67,17 @@ class PublicGitRepository extends Component
|
||||
|
||||
public function load_branch()
|
||||
{
|
||||
$this->branch_found = false;
|
||||
try {
|
||||
$this->branch_found = false;
|
||||
$this->validate([
|
||||
'repository_url' => 'required|url'
|
||||
]);
|
||||
$this->get_git_source();
|
||||
try {
|
||||
$this->get_branch();
|
||||
$this->selected_branch = $this->git_branch;
|
||||
$this->get_branch();
|
||||
$this->selected_branch = $this->git_branch;
|
||||
} catch (\Exception $e) {
|
||||
return general_error_handler(err: $e, that: $this);
|
||||
}
|
||||
|
||||
if (!$this->branch_found && $this->git_branch == 'main') {
|
||||
try {
|
||||
$this->git_branch = 'master';
|
||||
@@ -103,6 +102,9 @@ class PublicGitRepository extends Component
|
||||
} elseif ($this->git_host == 'bitbucket.org') {
|
||||
// Not supported yet
|
||||
}
|
||||
if (is_null($this->git_source)) {
|
||||
throw new \Exception('Git source not found. What?!');
|
||||
}
|
||||
}
|
||||
|
||||
private function get_branch()
|
||||
|
||||
@@ -3,45 +3,79 @@
|
||||
namespace App\Http\Livewire\Project\New;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use Countable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
use Route;
|
||||
|
||||
class Select extends Component
|
||||
{
|
||||
public $current_step = 'type';
|
||||
public ?int $server = null;
|
||||
public string $type;
|
||||
public string $server_id;
|
||||
public string $destination_uuid;
|
||||
public Countable|array|Server $servers;
|
||||
public $destinations = [];
|
||||
public Collection|array $standaloneDockers = [];
|
||||
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->setServer($foundServer);
|
||||
}
|
||||
}
|
||||
$this->current_step = 'servers';
|
||||
}
|
||||
|
||||
public function set_server(Server $server)
|
||||
public function setServer(Server $server)
|
||||
{
|
||||
$this->server_id = $server->id;
|
||||
$this->destinations = $server->destinations();
|
||||
$this->standaloneDockers = $server->standaloneDockers;
|
||||
$this->swarmDockers = $server->swarmDockers;
|
||||
$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', [
|
||||
|
||||
20
app/Http/Livewire/Server/All.php
Normal file
20
app/Http/Livewire/Server/All.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
class All extends Component
|
||||
{
|
||||
public ?Collection $servers = null;
|
||||
|
||||
public function mount () {
|
||||
$this->servers = Server::ownedByCurrentTeam()->get();
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.all');
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@ namespace App\Http\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\InstallDocker;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class Form extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
public Server $server;
|
||||
public $uptime;
|
||||
public $dockerVersion;
|
||||
@@ -64,14 +66,20 @@ class Form extends Component
|
||||
|
||||
public function delete()
|
||||
{
|
||||
if (!$this->server->isEmpty()) {
|
||||
$this->emit('error', 'Server has defined resources. Please delete them first.');
|
||||
return;
|
||||
try {
|
||||
$this->authorize('delete', $this->server);
|
||||
if (!$this->server->isEmpty()) {
|
||||
$this->emit('error', 'Server has defined resources. Please delete them first.');
|
||||
return;
|
||||
}
|
||||
$this->server->delete();
|
||||
return redirect()->route('server.all');
|
||||
} catch (\Exception $e) {
|
||||
return general_error_handler(err: $e, that: $this);
|
||||
}
|
||||
$this->server->delete();
|
||||
redirect()->route('server.all');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
public function submit()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@ class Status extends Component
|
||||
|
||||
public function get_status()
|
||||
{
|
||||
dispatch_sync(new ProxyContainerStatusJob(
|
||||
server: $this->server
|
||||
));
|
||||
$this->server->refresh();
|
||||
$this->emit('proxyStatusUpdated');
|
||||
if (data_get($this->server,'settings.is_usable')) {
|
||||
dispatch_sync(new ProxyContainerStatusJob(
|
||||
server: $this->server
|
||||
));
|
||||
$this->server->refresh();
|
||||
$this->emit('proxyStatusUpdated');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
app/Http/Livewire/Server/Show.php
Normal file
25
app/Http/Livewire/Server/Show.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class Show extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
public ?Server $server = null;
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->firstOrFail();
|
||||
} catch (\Throwable $e) {
|
||||
return general_error_handler(err: $e, that: $this);
|
||||
}
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.show');
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
use Masmerise\Toaster\Toaster;
|
||||
|
||||
class PrivateKey extends Component
|
||||
class ShowPrivateKey extends Component
|
||||
{
|
||||
public Server $server;
|
||||
public $privateKeys;
|
||||
@@ -20,6 +20,9 @@ class Email extends Component
|
||||
'settings.smtp_timeout' => 'nullable',
|
||||
'settings.smtp_from_address' => 'required|email',
|
||||
'settings.smtp_from_name' => 'required',
|
||||
'settings.resend_enabled' => 'nullable|boolean',
|
||||
'settings.resend_api_key' => 'nullable'
|
||||
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
'settings.smtp_from_address' => 'From Address',
|
||||
@@ -30,48 +33,68 @@ class Email extends Component
|
||||
'settings.smtp_encryption' => 'Encryption',
|
||||
'settings.smtp_username' => 'Username',
|
||||
'settings.smtp_password' => 'Password',
|
||||
'settings.smtp_timeout' => 'Timeout',
|
||||
'settings.resend_api_key' => 'Resend API Key'
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->decrypt();
|
||||
$this->emails = auth()->user()->email;
|
||||
}
|
||||
|
||||
private function decrypt()
|
||||
{
|
||||
if (data_get($this->settings, 'smtp_password')) {
|
||||
try {
|
||||
$this->settings->smtp_password = decrypt($this->settings->smtp_password);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
public function submitFromFields() {
|
||||
try {
|
||||
$this->resetErrorBag();
|
||||
$this->validate([
|
||||
'settings.smtp_from_address' => 'required|email',
|
||||
'settings.smtp_from_name' => 'required',
|
||||
]);
|
||||
$this->settings->save();
|
||||
$this->emit('success', 'Settings saved successfully.');
|
||||
} catch (\Exception $e) {
|
||||
return general_error_handler($e, $this);
|
||||
}
|
||||
}
|
||||
public function submitResend() {
|
||||
try {
|
||||
$this->resetErrorBag();
|
||||
$this->validate([
|
||||
'settings.resend_api_key' => 'required'
|
||||
]);
|
||||
$this->settings->smtp_enabled = false;
|
||||
$this->settings->save();
|
||||
$this->emit('success', 'Settings saved successfully.');
|
||||
} catch (\Exception $e) {
|
||||
$this->settings->resend_enabled = false;
|
||||
return general_error_handler($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->submit();
|
||||
$this->emit('success', 'Settings saved successfully.');
|
||||
} catch (\Exception $e) {
|
||||
$this->settings->smtp_enabled = false;
|
||||
$this->validate();
|
||||
return general_error_handler($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->resetErrorBag();
|
||||
$this->validate();
|
||||
if ($this->settings->smtp_password) {
|
||||
$this->settings->smtp_password = encrypt($this->settings->smtp_password);
|
||||
} else {
|
||||
$this->settings->smtp_password = null;
|
||||
try {
|
||||
$this->resetErrorBag();
|
||||
$this->validate([
|
||||
'settings.smtp_host' => 'required',
|
||||
'settings.smtp_port' => 'required|numeric',
|
||||
'settings.smtp_encryption' => 'nullable',
|
||||
'settings.smtp_username' => 'nullable',
|
||||
'settings.smtp_password' => 'nullable',
|
||||
'settings.smtp_timeout' => 'nullable',
|
||||
]);
|
||||
$this->settings->resend_enabled = false;
|
||||
$this->settings->save();
|
||||
$this->emit('success', 'Settings saved successfully.');
|
||||
} catch (\Exception $e) {
|
||||
return general_error_handler($e, $this);
|
||||
}
|
||||
|
||||
$this->settings->save();
|
||||
$this->emit('success', 'Transaction email settings updated successfully.');
|
||||
$this->decrypt();
|
||||
}
|
||||
|
||||
public function sendTestNotification()
|
||||
|
||||
@@ -37,9 +37,13 @@ class Change extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->webhook_endpoint = $this->ipv4;
|
||||
if (isCloud() && !isDev()) {
|
||||
$this->webhook_endpoint = config('app.url');
|
||||
} else {
|
||||
$this->webhook_endpoint = $this->ipv4;
|
||||
$this->is_system_wide = $this->github_app->is_system_wide;
|
||||
}
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->is_system_wide = $this->github_app->is_system_wide;
|
||||
}
|
||||
|
||||
public function submit()
|
||||
|
||||
@@ -32,16 +32,22 @@ 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]]);
|
||||
}
|
||||
redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]);
|
||||
} catch (\Exception $e) {
|
||||
return general_error_handler(err: $e, that: $this);
|
||||
|
||||
@@ -47,9 +47,12 @@ class PricingPlans extends Component
|
||||
'tax_id_collection' => [
|
||||
'enabled' => true,
|
||||
],
|
||||
'automatic_tax' => [
|
||||
'enabled' => true,
|
||||
],
|
||||
'mode' => 'subscription',
|
||||
'success_url' => route('subscription.success'),
|
||||
'cancel_url' => route('subscription.show',['cancelled' => true]),
|
||||
'success_url' => route('dashboard', ['success' => true]),
|
||||
'cancel_url' => route('subscription.index', ['cancelled' => true]),
|
||||
];
|
||||
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
|
||||
if ($customer) {
|
||||
|
||||
@@ -30,7 +30,7 @@ class Create extends Component
|
||||
]);
|
||||
auth()->user()->teams()->attach($team, ['role' => 'admin']);
|
||||
refreshSession();
|
||||
return redirect()->route('team.show');
|
||||
return redirect()->route('team.index');
|
||||
} catch (\Throwable $th) {
|
||||
return general_error_handler($th, $this);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ class Delete extends Component
|
||||
});
|
||||
|
||||
refreshSession();
|
||||
return redirect()->route('team.show');
|
||||
return redirect()->route('team.index');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ class Form extends Component
|
||||
try {
|
||||
$this->team->save();
|
||||
refreshSession();
|
||||
$this->emit('reloadWindow');
|
||||
} catch (\Throwable $th) {
|
||||
return general_error_handler($th, $this);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class Invitations extends Component
|
||||
{
|
||||
TeamInvitation::find($invitation_id)->delete();
|
||||
$this->refreshInvitations();
|
||||
$this->emit('success', 'Invitation revoked.');
|
||||
}
|
||||
|
||||
public function refreshInvitations()
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire;
|
||||
namespace App\Http\Livewire\Waitlist;
|
||||
|
||||
use App\Jobs\SendConfirmationForWaitlistJob;
|
||||
use App\Models\User;
|
||||
use App\Models\Waitlist as ModelsWaitlist;
|
||||
use App\Models\Waitlist;
|
||||
use Livewire\Component;
|
||||
|
||||
class Waitlist extends Component
|
||||
class Index extends Component
|
||||
{
|
||||
public string $email;
|
||||
public int $waiting_in_line = 0;
|
||||
public int $waitingInLine = 0;
|
||||
|
||||
protected $rules = [
|
||||
'email' => 'required|email',
|
||||
];
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.waitlist.index')->layout('layouts.simple');
|
||||
}
|
||||
public function mount()
|
||||
{
|
||||
$this->waitingInLine = Waitlist::whereVerified(true)->count();
|
||||
if (isDev()) {
|
||||
$this->email = 'waitlist@example.com';
|
||||
}
|
||||
@@ -29,7 +34,7 @@ class Waitlist extends Component
|
||||
if ($already_registered) {
|
||||
throw new \Exception('You are already on the waitlist or registered. <br>Please check your email to verify your email address or contact support.');
|
||||
}
|
||||
$found = ModelsWaitlist::where('email', $this->email)->first();
|
||||
$found = Waitlist::where('email', $this->email)->first();
|
||||
if ($found) {
|
||||
if (!$found->verified) {
|
||||
$this->emit('error', 'You are already on the waitlist. <br>Please check your email to verify your email address.');
|
||||
@@ -38,7 +43,7 @@ class Waitlist extends Component
|
||||
$this->emit('error', 'You are already on the waitlist. <br>You will be notified when your turn comes. <br>Thank you.');
|
||||
return;
|
||||
}
|
||||
$waitlist = ModelsWaitlist::create([
|
||||
$waitlist = Waitlist::create([
|
||||
'email' => $this->email,
|
||||
'type' => 'registration',
|
||||
]);
|
||||
@@ -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') {
|
||||
|
||||
@@ -15,7 +15,7 @@ class IsBoardingFlow
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// ray('IsBoardingFlow Middleware');
|
||||
ray()->showQueries()->color('orange');
|
||||
if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
|
||||
return redirect('boarding');
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SubscriptionValid
|
||||
class IsSubscriptionValid
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (isInstanceAdmin()) {
|
||||
return $next($request);
|
||||
}
|
||||
if (!auth()->user() || !is_cloud()) {
|
||||
if (!auth()->user() || !isCloud()) {
|
||||
if ($request->path() === 'subscription') {
|
||||
return redirect('/');
|
||||
} else {
|
||||
@@ -20,6 +20,7 @@ use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -65,6 +66,7 @@ class ApplicationDeploymentJob implements ShouldQueue
|
||||
private $log_model;
|
||||
private Collection $saved_outputs;
|
||||
|
||||
public $tries = 1;
|
||||
public function __construct(int $application_deployment_queue_id)
|
||||
{
|
||||
ray()->clearScreen();
|
||||
@@ -174,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();
|
||||
@@ -199,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) {
|
||||
@@ -235,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(
|
||||
@@ -257,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')) {
|
||||
@@ -275,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}.'",
|
||||
@@ -297,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,
|
||||
],
|
||||
[
|
||||
@@ -487,7 +496,7 @@ class ApplicationDeploymentJob implements ShouldQueue
|
||||
],
|
||||
'networks' => [
|
||||
$this->destination->network => [
|
||||
'external' => false,
|
||||
'external' => true,
|
||||
'name' => $this->destination->network,
|
||||
'attachable' => true,
|
||||
]
|
||||
@@ -647,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}
|
||||
@@ -685,12 +694,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$this->execute_in_builder("echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf")
|
||||
],
|
||||
[
|
||||
$this->execute_in_builder("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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -699,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],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class CleanupInstanceStuffsJob implements ShouldQueue, ShouldBeUnique
|
||||
|
||||
private function cleanup_waitlist()
|
||||
{
|
||||
$waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.confirmation_valid_for_minutes')))->get();
|
||||
$waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.expiration')))->get();
|
||||
foreach ($waitlist as $item) {
|
||||
$item->delete();
|
||||
}
|
||||
|
||||
@@ -77,7 +77,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 +107,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,64 +2,88 @@
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DockerCleanupJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 500;
|
||||
public ?string $dockerRootFilesystem = null;
|
||||
public ?int $usageBefore = null;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function middleware(): array
|
||||
{
|
||||
return [
|
||||
(new WithoutOverlapping("dockerimagejobs"))->shared(),
|
||||
];
|
||||
}
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$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();
|
||||
foreach ($servers as $server) {
|
||||
if (isDev()) {
|
||||
$docker_root_filesystem = "/";
|
||||
} else {
|
||||
$docker_root_filesystem = instant_remote_process(['stat --printf=%m $(docker info --format "{{json .DockerRootDir}}" |sed \'s/"//g\')'], $server);
|
||||
if (
|
||||
!$server->settings->is_reachable && !$server->settings->is_usable
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
$disk_percentage_before = $this->get_disk_usage($server, $docker_root_filesystem);
|
||||
if ($disk_percentage_before >= $server->settings->cleanup_after_percentage) {
|
||||
if (isDev()) {
|
||||
$this->dockerRootFilesystem = "/";
|
||||
} else {
|
||||
$this->dockerRootFilesystem = instant_remote_process(
|
||||
[
|
||||
"stat --printf=%m $(docker info --format '{{json .DockerRootDir}}'' |sed 's/\"//g')"
|
||||
],
|
||||
$server,
|
||||
false
|
||||
);
|
||||
}
|
||||
if (!$this->dockerRootFilesystem) {
|
||||
continue;
|
||||
}
|
||||
$this->usageBefore = $this->getFilesystemUsage($server);
|
||||
if ($this->usageBefore >= $server->settings->cleanup_after_percentage) {
|
||||
ray('Cleaning up ' . $server->name)->color('orange');
|
||||
instant_remote_process(['docker image prune -af'], $server);
|
||||
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server);
|
||||
instant_remote_process(['docker builder prune -af'], $server);
|
||||
$disk_percentage_after = $this->get_disk_usage($server, $docker_root_filesystem);
|
||||
if ($disk_percentage_after < $disk_percentage_before) {
|
||||
ray('Saved ' . ($disk_percentage_before - $disk_percentage_after) . '% disk space on ' . $server->name);
|
||||
$usageAfter = $this->getFilesystemUsage($server);
|
||||
if ($usageAfter < $this->usageBefore) {
|
||||
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $server->name)->color('orange');
|
||||
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $server->name);
|
||||
} else {
|
||||
ray('DockerCleanupJob failed to save disk space on ' . $server->name)->color('orange');
|
||||
}
|
||||
} else {
|
||||
ray('No need to clean up ' . $server->name)->color('orange');
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage());
|
||||
ray($e->getMessage());
|
||||
ray($e->getMessage())->color('orange');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function get_disk_usage(Server $server, string $docker_root_filesystem)
|
||||
private function getFilesystemUsage(Server $server)
|
||||
{
|
||||
$disk_usage = json_decode(instant_remote_process(['df -hP | awk \'BEGIN {printf"{\\"disks\\":["}{if($1=="Filesystem")next;if(a)printf",";printf"{\\"mount\\":\\""$6"\\",\\"size\\":\\""$2"\\",\\"used\\":\\""$3"\\",\\"avail\\":\\""$4"\\",\\"use%\\":\\""$5"\\"}";a++;}END{print"]}";}\''], $server), true);
|
||||
$mount_point = collect(data_get($disk_usage, 'disks'))->where('mount', $docker_root_filesystem)->first();
|
||||
ray($mount_point);
|
||||
return Str::of(data_get($mount_point, 'use%'))->trim()->replace('%', '')->value();
|
||||
return instant_remote_process(["df '{$this->dockerRootFilesystem}'| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,9 @@ class ProxyCheckJob implements ShouldQueue
|
||||
if ($status === 'running') {
|
||||
continue;
|
||||
}
|
||||
// $server->team->notify(new ProxyStoppedNotification($server));
|
||||
resolve(StartProxy::class)($server);
|
||||
if (data_get($server, 'proxy.type')) {
|
||||
resolve(StartProxy::class)($server);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
ray($th->getMessage());
|
||||
|
||||
@@ -39,9 +39,9 @@ class ProxyContainerStatusJob implements ShouldQueue, ShouldBeUnique
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$container = getContainerStatus(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: true);
|
||||
$container = getContainerStatus(server: $this->server, all_data: true, container_id: 'coolify-proxy', throwError: false);
|
||||
$status = data_get($container, 'State.Status');
|
||||
if (data_get($this->server,'proxy.status') !== $status) {
|
||||
if ($status && data_get($this->server, 'proxy.status') !== $status) {
|
||||
$this->server->proxy->status = $status;
|
||||
if ($this->server->proxy->status === 'running') {
|
||||
$traefik = $container['Config']['Labels']['org.opencontainers.image.title'];
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
@@ -27,6 +29,11 @@ class ProxyStartJob implements ShouldQueue
|
||||
if ($status === 'running') {
|
||||
return;
|
||||
}
|
||||
if (is_null(data_get($this->server, 'proxy.type'))) {
|
||||
$this->server->proxy->type = ProxyTypes::TRAEFIK_V2->value;
|
||||
$this->server->proxy->status = ProxyStatus::EXITED->value;
|
||||
$this->server->save();
|
||||
}
|
||||
resolve(StartProxy::class)($this->server);
|
||||
} catch (\Throwable $th) {
|
||||
send_internal_notification('ProxyStartJob failed with: ' . $th->getMessage());
|
||||
|
||||
@@ -37,7 +37,7 @@ class SendConfirmationForWaitlistJob implements ShouldQueue
|
||||
$mail->subject('You are on the waitlist!');
|
||||
send_user_an_email($mail, $this->email);
|
||||
} catch (\Throwable $th) {
|
||||
send_internal_notification('SendConfirmationForWaitlistJob failed with error: ' . $th->getMessage());
|
||||
send_internal_notification("SendConfirmationForWaitlistJob failed for {$mail} with error: " . $th->getMessage());
|
||||
ray($th->getMessage());
|
||||
throw $th;
|
||||
}
|
||||
|
||||
72
app/Jobs/SendMessageToTelegramJob.php
Normal file
72
app/Jobs/SendMessageToTelegramJob.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Str;
|
||||
|
||||
class SendMessageToTelegramJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* The number of times the job may be attempted.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $tries = 5;
|
||||
|
||||
/**
|
||||
* The maximum number of unhandled exceptions to allow before failing.
|
||||
*/
|
||||
public int $maxExceptions = 3;
|
||||
|
||||
public function __construct(
|
||||
public string $text,
|
||||
public array $buttons,
|
||||
public string $token,
|
||||
public string $chatId,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$url = 'https://api.telegram.org/bot' . $this->token . '/sendMessage';
|
||||
$inlineButtons = [];
|
||||
if (!empty($this->buttons)) {
|
||||
foreach ($this->buttons as $button) {
|
||||
$buttonUrl = data_get($button, 'url');
|
||||
if ($buttonUrl && Str::contains($buttonUrl, 'http://localhost')) {
|
||||
$buttonUrl = str_replace('http://localhost', config('app.url'), $buttonUrl);
|
||||
}
|
||||
$inlineButtons[] = [
|
||||
'text' => $button['text'],
|
||||
'url' => $buttonUrl,
|
||||
];
|
||||
}
|
||||
}
|
||||
$payload = [
|
||||
'parse_mode' => 'markdown',
|
||||
'reply_markup' => json_encode([
|
||||
'inline_keyboard' => [
|
||||
[...$inlineButtons],
|
||||
],
|
||||
]),
|
||||
'chat_id' => $this->chatId,
|
||||
'text' => $this->text,
|
||||
];
|
||||
ray($payload);
|
||||
$response = Http::post($url, $payload);
|
||||
if ($response->failed()) {
|
||||
throw new \Exception('Telegram notification failed with ' . $response->status() . ' status code.' . $response->body());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Stripe\Stripe;
|
||||
|
||||
class SubscriptionInvoiceFailedJob implements ShouldQueue
|
||||
{
|
||||
|
||||
@@ -4,15 +4,7 @@ namespace App\Models;
|
||||
|
||||
class ApplicationPreview extends BaseModel
|
||||
{
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
'pull_request_id',
|
||||
'pull_request_html_url',
|
||||
'pull_request_issue_comment_id',
|
||||
'fqdn',
|
||||
'status',
|
||||
'application_id',
|
||||
];
|
||||
protected $guarded = [];
|
||||
|
||||
static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id)
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ class InstanceSettings extends Model implements SendsEmail
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
'resale_license' => 'encrypted',
|
||||
'smtp_password' => 'encrypted',
|
||||
];
|
||||
|
||||
public static function get()
|
||||
|
||||
@@ -33,6 +33,9 @@ class Server extends BaseModel
|
||||
|
||||
});
|
||||
static::deleting(function ($server) {
|
||||
$server->destinations()->each(function ($destination) {
|
||||
$destination->delete();
|
||||
});
|
||||
$server->settings()->delete();
|
||||
});
|
||||
}
|
||||
@@ -70,8 +73,6 @@ class Server extends BaseModel
|
||||
return $standaloneDocker->concat($swarmDocker);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function settings()
|
||||
{
|
||||
return $this->hasOne(ServerSetting::class);
|
||||
@@ -84,12 +85,20 @@ class Server extends BaseModel
|
||||
|
||||
public function isEmpty()
|
||||
{
|
||||
if ($this->applications()->count() === 0) {
|
||||
$applications = $this->applications()->count() === 0;
|
||||
$databases = $this->databases()->count() === 0;
|
||||
if ($applications && $databases) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function databases() {
|
||||
return $this->destinations()->map(function ($standaloneDocker) {
|
||||
$postgresqls = $standaloneDocker->postgresqls;
|
||||
return $postgresqls?->concat([]) ?? collect([]);
|
||||
})->flatten();
|
||||
}
|
||||
public function applications()
|
||||
{
|
||||
return $this->destinations()->map(function ($standaloneDocker) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Subscription extends Model
|
||||
{
|
||||
@@ -14,19 +15,44 @@ class Subscription extends Model
|
||||
}
|
||||
public function type()
|
||||
{
|
||||
$basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids'));
|
||||
$pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids'));
|
||||
$ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids'));
|
||||
if (isLemon()) {
|
||||
$basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids'));
|
||||
$pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids'));
|
||||
$ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids'));
|
||||
|
||||
$subscription = $this->lemon_variant_id;
|
||||
if (in_array($subscription, $basic)) {
|
||||
return 'basic';
|
||||
$subscription = $this->lemon_variant_id;
|
||||
if (in_array($subscription, $basic)) {
|
||||
return 'basic';
|
||||
}
|
||||
if (in_array($subscription, $pro)) {
|
||||
return 'pro';
|
||||
}
|
||||
if (in_array($subscription, $ultimate)) {
|
||||
return 'ultimate';
|
||||
}
|
||||
}
|
||||
if (in_array($subscription, $pro)) {
|
||||
return 'pro';
|
||||
}
|
||||
if (in_array($subscription, $ultimate)) {
|
||||
return 'ultimate';
|
||||
if (isStripe()) {
|
||||
if (!$this->stripe_plan_id) {
|
||||
return 'unknown';
|
||||
}
|
||||
$subscription = Subscription::where('id', $this->id)->first();
|
||||
if (!$subscription) {
|
||||
return null;
|
||||
}
|
||||
$subscriptionPlanId = data_get($subscription,'stripe_plan_id');
|
||||
if (!$subscriptionPlanId) {
|
||||
return null;
|
||||
}
|
||||
$subscriptionConfigs = collect(config('subscription'));
|
||||
$stripePlanId = null;
|
||||
$subscriptionConfigs->map(function ($value, $key) use ($subscriptionPlanId, &$stripePlanId) {
|
||||
if ($value === $subscriptionPlanId){
|
||||
$stripePlanId = $key;
|
||||
};
|
||||
})->first();
|
||||
if ($stripePlanId) {
|
||||
return Str::of($stripePlanId)->after('stripe_price_id_')->before('_')->lower();
|
||||
}
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use App\Notifications\Channels\SendsDiscord;
|
||||
use App\Notifications\Channels\SendsEmail;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
@@ -14,6 +15,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
'personal_team' => 'boolean',
|
||||
'smtp_password' => 'encrypted',
|
||||
'resend_api_key' => 'encrypted',
|
||||
];
|
||||
|
||||
public function routeNotificationForDiscord()
|
||||
@@ -21,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);
|
||||
@@ -30,6 +41,27 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
}
|
||||
return explode(',', $recipients);
|
||||
}
|
||||
public function limits(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
if (config('coolify.self_hosted') || $this->id === 0) {
|
||||
$subscription = 'self-hosted';
|
||||
} else {
|
||||
$subscription = data_get($this, 'subscription');
|
||||
if (is_null($subscription)) {
|
||||
$subscription = 'zero';
|
||||
} else {
|
||||
$subscription = $subscription->type();
|
||||
}
|
||||
}
|
||||
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
|
||||
$sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)];
|
||||
return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled];
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
public function members()
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ class User extends Authenticatable implements SendsEmail
|
||||
$team = [
|
||||
'name' => $user->name . "'s Team",
|
||||
'personal_team' => true,
|
||||
'show_boarding' => true
|
||||
];
|
||||
if ($user->id === 0) {
|
||||
$team['id'] = 0;
|
||||
@@ -91,29 +92,20 @@ class User extends Authenticatable implements SendsEmail
|
||||
return $found_root_team->count() > 0;
|
||||
}
|
||||
|
||||
public function personalTeam()
|
||||
{
|
||||
return $this->teams()->where('personal_team', true)->first();
|
||||
}
|
||||
|
||||
public function currentTeam()
|
||||
{
|
||||
return $this->teams()->where('team_id', session('currentTeam')->id)->first();
|
||||
return Team::find(session('currentTeam')->id);
|
||||
}
|
||||
|
||||
public function otherTeams()
|
||||
{
|
||||
$team_id = currentTeam()->id;
|
||||
return auth()->user()->teams->filter(function ($team) use ($team_id) {
|
||||
return $team->id != $team_id;
|
||||
return auth()->user()->teams->filter(function ($team) {
|
||||
return $team->id != currentTeam()->id;
|
||||
});
|
||||
}
|
||||
|
||||
public function role()
|
||||
{
|
||||
if ($this->teams()->where('team_id', 0)->first()) {
|
||||
return 'admin';
|
||||
}
|
||||
return $this->teams()->where('team_id', currentTeam()->id)->first()->pivot->role;
|
||||
return session('currentTeam')->pivot->role;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,15 @@ class DeploymentFailed extends Notification implements ShouldQueue
|
||||
|
||||
public Application $application;
|
||||
public string $deployment_uuid;
|
||||
public ApplicationPreview|null $preview;
|
||||
public ?ApplicationPreview $preview = null;
|
||||
|
||||
public string $application_name;
|
||||
public string|null $deployment_url = null;
|
||||
public ?string $deployment_url = null;
|
||||
public string $project_uuid;
|
||||
public string $environment_name;
|
||||
public string|null $fqdn;
|
||||
public ?string $fqdn = null;
|
||||
|
||||
public function __construct(Application $application, string $deployment_uuid, ApplicationPreview|null $preview)
|
||||
public function __construct(Application $application, string $deployment_uuid, ?ApplicationPreview $preview = null)
|
||||
{
|
||||
$this->application = $application;
|
||||
$this->deployment_uuid = $deployment_uuid;
|
||||
@@ -43,19 +43,7 @@ class DeploymentFailed 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
|
||||
@@ -67,9 +55,8 @@ class DeploymentFailed extends Notification implements ShouldQueue
|
||||
$mail->subject('❌ Deployment failed of ' . $this->application_name . '.');
|
||||
} else {
|
||||
$fqdn = $this->preview->fqdn;
|
||||
$mail->subject('❌ Pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . ' deployment failed.');
|
||||
$mail->subject('❌ Deployment failed of pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . '.');
|
||||
}
|
||||
|
||||
$mail->view('emails.application-deployment-failed', [
|
||||
'name' => $this->application_name,
|
||||
'fqdn' => $fqdn,
|
||||
@@ -90,4 +77,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
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,19 +43,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 +87,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
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,19 +37,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 +58,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
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use Exception;
|
||||
use Illuminate\Mail\Message;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EmailChannel
|
||||
{
|
||||
@@ -24,11 +23,7 @@ class EmailChannel
|
||||
[],
|
||||
[],
|
||||
fn (Message $message) => $message
|
||||
->from(
|
||||
data_get($notifiable, 'smtp_from_address'),
|
||||
data_get($notifiable, 'smtp_from_name'),
|
||||
)
|
||||
->bcc($recepients)
|
||||
->to($recepients)
|
||||
->subject($mailMessage->subject)
|
||||
->html((string)$mailMessage->render())
|
||||
);
|
||||
@@ -36,13 +31,20 @@ class EmailChannel
|
||||
|
||||
private function bootConfigs($notifiable): void
|
||||
{
|
||||
$password = data_get($notifiable, 'smtp_password');
|
||||
if ($password) $password = decrypt($password);
|
||||
|
||||
if (Str::contains(data_get($notifiable, 'smtp_host'),'resend.com')) {
|
||||
if (data_get($notifiable, 'use_instance_email_settings')) {
|
||||
$type = set_transanctional_email_settings();
|
||||
if (!$type) {
|
||||
throw new Exception('No email settings found.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
config()->set('mail.from.address', data_get($notifiable, 'smtp_from_address'));
|
||||
config()->set('mail.from.name', data_get($notifiable, 'smtp_from_name'));
|
||||
if (data_get($notifiable, 'resend_enabled')) {
|
||||
config()->set('mail.default', 'resend');
|
||||
config()->set('resend.api_key', $password);
|
||||
} else {
|
||||
config()->set('resend.api_key', data_get($notifiable, 'resend_api_key'));
|
||||
}
|
||||
if (data_get($notifiable, 'smtp_enabled')) {
|
||||
config()->set('mail.default', 'smtp');
|
||||
config()->set('mail.mailers.smtp', [
|
||||
"transport" => "smtp",
|
||||
@@ -50,7 +52,7 @@ class EmailChannel
|
||||
"port" => data_get($notifiable, 'smtp_port'),
|
||||
"encryption" => data_get($notifiable, 'smtp_encryption'),
|
||||
"username" => data_get($notifiable, 'smtp_username'),
|
||||
"password" => $password,
|
||||
"password" => data_get($notifiable, 'smtp_password'),
|
||||
"timeout" => data_get($notifiable, 'smtp_timeout'),
|
||||
"local_domain" => null,
|
||||
]);
|
||||
|
||||
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();
|
||||
|
||||
}
|
||||
25
app/Notifications/Channels/TelegramChannel.php
Normal file
25
app/Notifications/Channels/TelegramChannel.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications\Channels;
|
||||
|
||||
use App\Jobs\SendMessageToTelegramJob;
|
||||
|
||||
class TelegramChannel
|
||||
{
|
||||
public function send($notifiable, $notification): void
|
||||
{
|
||||
$data = $notification->toTelegram($notifiable);
|
||||
$telegramData = $notifiable->routeNotificationForTelegram();
|
||||
|
||||
$message = data_get($data, 'message');
|
||||
$buttons = data_get($data, 'buttons', []);
|
||||
ray($message, $buttons);
|
||||
$telegramToken = data_get($telegramData, 'token');
|
||||
$chatId = data_get($telegramData, 'chat_id');
|
||||
|
||||
if (!$telegramToken || !$chatId || !$message) {
|
||||
throw new \Exception('Telegram token, chat id and message are required');
|
||||
}
|
||||
dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId));
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,19 @@ namespace App\Notifications\Channels;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\User;
|
||||
use Exception;
|
||||
use Illuminate\Mail\Message;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Log;
|
||||
|
||||
class TransactionalEmailChannel
|
||||
{
|
||||
public function send(User $notifiable, Notification $notification): void
|
||||
{
|
||||
$settings = InstanceSettings::get();
|
||||
if (data_get($settings, 'smtp_enabled') !== true) {
|
||||
if (!data_get($settings, 'smtp_enabled') && !data_get($settings, 'resend_enabled')) {
|
||||
Log::info('SMTP/Resend not enabled');
|
||||
return;
|
||||
}
|
||||
$email = $notifiable->email;
|
||||
@@ -26,10 +29,6 @@ class TransactionalEmailChannel
|
||||
[],
|
||||
[],
|
||||
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())
|
||||
@@ -38,6 +37,9 @@ class TransactionalEmailChannel
|
||||
|
||||
private function bootConfigs(): void
|
||||
{
|
||||
set_transanctional_email_settings();
|
||||
$type = set_transanctional_email_settings();
|
||||
if (!$type) {
|
||||
throw new Exception('No email settings found.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,42 +14,41 @@ class BackupFailed extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public string $message = 'Backup FAILED';
|
||||
|
||||
public string $name;
|
||||
public string $frequency;
|
||||
|
||||
public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output)
|
||||
{
|
||||
$this->message = "❌ Database backup for {$database->name} with frequency of $backup->frequency was FAILED.\n\nReason: $output";
|
||||
$this->name = $database->name;
|
||||
$this->frequency = $backup->frequency;
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
|
||||
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
|
||||
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups');
|
||||
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups');
|
||||
|
||||
if ($isEmailEnabled && $isSubscribedToEmailEvent) {
|
||||
$channels[] = EmailChannel::class;
|
||||
}
|
||||
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
|
||||
$channels[] = DiscordChannel::class;
|
||||
}
|
||||
ray($channels);
|
||||
return $channels;
|
||||
return setNotificationChannels($notifiable, 'database_backups');
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$mail = new MailMessage();
|
||||
$mail->subject("❌ Backup FAILED for {$this->database->name}");
|
||||
$mail->line($this->message);
|
||||
$mail->subject("❌ [ACTION REQUIRED] Backup FAILED for {$this->database->name}");
|
||||
$mail->view('emails.backup-failed', [
|
||||
'name' => $this->name,
|
||||
'frequency' => $this->frequency,
|
||||
'output' => $this->output,
|
||||
]);
|
||||
return $mail;
|
||||
}
|
||||
|
||||
public function toDiscord(): string
|
||||
{
|
||||
return $this->message;
|
||||
return "❌ Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
|
||||
}
|
||||
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,41 +14,40 @@ class BackupSuccess extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public string $message = 'Backup Success';
|
||||
|
||||
public string $name;
|
||||
public string $frequency;
|
||||
|
||||
public function __construct(ScheduledDatabaseBackup $backup, public $database)
|
||||
{
|
||||
$this->message = "✅ Database backup for {$database->name} with frequency of $backup->frequency was successful.";
|
||||
$this->name = $database->name;
|
||||
$this->frequency = $backup->frequency;
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
|
||||
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
|
||||
$isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups');
|
||||
$isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups');
|
||||
|
||||
if ($isEmailEnabled && $isSubscribedToEmailEvent) {
|
||||
$channels[] = EmailChannel::class;
|
||||
}
|
||||
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
|
||||
$channels[] = DiscordChannel::class;
|
||||
}
|
||||
return $channels;
|
||||
return setNotificationChannels($notifiable, 'database_backups');
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$mail = new MailMessage();
|
||||
$mail->subject("✅ Backup success for {$this->database->name}");
|
||||
$mail->line($this->message);
|
||||
$mail->subject("✅ Backup successfully done for {$this->database->name}");
|
||||
$mail->view('emails.backup-success', [
|
||||
'name' => $this->name,
|
||||
'frequency' => $this->frequency,
|
||||
]);
|
||||
return $mail;
|
||||
}
|
||||
|
||||
public function toDiscord(): string
|
||||
{
|
||||
return $this->message;
|
||||
return "✅ Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
|
||||
}
|
||||
public function toTelegram(): array
|
||||
{
|
||||
$message = "✅ Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
|
||||
return [
|
||||
"message" => $message,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -12,16 +13,22 @@ class GeneralNotification extends Notification implements ShouldQueue
|
||||
use Queueable;
|
||||
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,19 +22,7 @@ class NotReachable 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
|
||||
@@ -55,4 +43,10 @@ class NotReachable extends Notification implements ShouldQueue
|
||||
$message = '⛔ Server \'' . $this->server->name . '\' is unreachable (could be a temporary issue). If you receive this more than twice in a row, please check your server.';
|
||||
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;
|
||||
@@ -19,24 +20,13 @@ class Test extends Notification implements ShouldQueue
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$channels = [];
|
||||
$isEmailEnabled = data_get($notifiable, 'smtp_enabled');
|
||||
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
|
||||
|
||||
if ($isDiscordEnabled && empty($this->emails)) {
|
||||
$channels[] = DiscordChannel::class;
|
||||
}
|
||||
|
||||
if ($isEmailEnabled && !empty($this->emails)) {
|
||||
$channels[] = EmailChannel::class;
|
||||
}
|
||||
return $channels;
|
||||
return setNotificationChannels($notifiable, 'test');
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$mail = new MailMessage();
|
||||
$mail->subject("Coolify Test Notification");
|
||||
$mail->subject("Test Email");
|
||||
$mail->view('emails.test');
|
||||
return $mail;
|
||||
}
|
||||
@@ -48,4 +38,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()
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,16 +20,19 @@ class InvitationLink extends Notification implements ShouldQueue
|
||||
return [TransactionalEmailChannel::class];
|
||||
}
|
||||
|
||||
public function toMail(User $user): MailMessage
|
||||
public function __construct(public User $user)
|
||||
{
|
||||
$invitation = TeamInvitation::whereEmail($user->email)->first();
|
||||
}
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$invitation = TeamInvitation::whereEmail($this->user->email)->first();
|
||||
$invitation_team = Team::find($invitation->team->id);
|
||||
|
||||
$mail = new MailMessage();
|
||||
$mail->subject('Invitation for ' . $invitation_team->name);
|
||||
$mail->view('emails.invitation-link', [
|
||||
'team' => $invitation_team->name,
|
||||
'email' => $user->email,
|
||||
'email' => $this->user->email,
|
||||
'invitation_link' => $invitation->link,
|
||||
]);
|
||||
return $mail;
|
||||
|
||||
@@ -31,24 +31,11 @@ class ResetPassword extends Notification
|
||||
|
||||
public function via($notifiable)
|
||||
{
|
||||
if ($this->settings->smtp_enabled) {
|
||||
$password = data_get($this->settings, 'smtp_password');
|
||||
if ($password) $password = decrypt($password);
|
||||
|
||||
config()->set('mail.default', 'smtp');
|
||||
config()->set('mail.mailers.smtp', [
|
||||
"transport" => "smtp",
|
||||
"host" => data_get($this->settings, 'smtp_host'),
|
||||
"port" => data_get($this->settings, 'smtp_port'),
|
||||
"encryption" => data_get($this->settings, 'smtp_encryption'),
|
||||
"username" => data_get($this->settings, 'smtp_username'),
|
||||
"password" => $password,
|
||||
"timeout" => data_get($this->settings, 'smtp_timeout'),
|
||||
"local_domain" => null,
|
||||
]);
|
||||
return ['mail'];
|
||||
$type = set_transanctional_email_settings();
|
||||
if (!$type) {
|
||||
throw new \Exception('No email settings found.');
|
||||
}
|
||||
throw new \Exception('SMTP is not enabled');
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail($notifiable)
|
||||
@@ -63,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;
|
||||
|
||||
@@ -24,7 +24,7 @@ class Test extends Notification implements ShouldQueue
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$mail = new MailMessage();
|
||||
$mail->subject('Test Notification');
|
||||
$mail->subject('Test Email');
|
||||
$mail->view('emails.test');
|
||||
return $mail;
|
||||
}
|
||||
|
||||
66
app/Policies/ServerPolicy.php
Normal file
66
app/Policies/ServerPolicy.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class ServerPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, Server $server): bool
|
||||
{
|
||||
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, Server $server): bool
|
||||
{
|
||||
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, Server $server): bool
|
||||
{
|
||||
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, Server $server): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, Server $server): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -43,22 +43,16 @@ class FortifyServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
|
||||
Fortify::createUsersUsing(CreateNewUser::class);
|
||||
Fortify::registerView(function () {
|
||||
$settings = InstanceSettings::get();
|
||||
$waiting_in_line = Waitlist::whereVerified(true)->count();
|
||||
if (!$settings->is_registration_enabled) {
|
||||
return redirect()->route('login');
|
||||
}
|
||||
if (config('coolify.waitlist')) {
|
||||
return view('auth.waitlist',[
|
||||
'waiting_in_line' => $waiting_in_line,
|
||||
]);
|
||||
return redirect()->route('waitlist.index');
|
||||
} else {
|
||||
return view('auth.register',[
|
||||
'waiting_in_line' => $waiting_in_line,
|
||||
]);
|
||||
return view('auth.register');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ trait ExecuteRemoteCommand
|
||||
|
||||
$remote_command = generate_ssh_command($private_key_location, $ip, $user, $port, $command);
|
||||
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) {
|
||||
$output = Str::of($output)->trim();
|
||||
$new_log_entry = [
|
||||
'command' => $command,
|
||||
'output' => $output,
|
||||
|
||||
25
app/Traits/SaveFromRedirect.php
Normal file
25
app/Traits/SaveFromRedirect.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
trait SaveFromRedirect
|
||||
{
|
||||
public function saveFromRedirect(string $route, ?Collection $parameters = null)
|
||||
{
|
||||
session()->forget('from');
|
||||
if (!$parameters || $parameters->count() === 0) {
|
||||
$parameters = $this->parameters;
|
||||
}
|
||||
$parameters = collect($parameters) ?? collect([]);
|
||||
$queries = collect($this->query) ?? collect([]);
|
||||
$parameters = $parameters->merge($queries);
|
||||
session(['from' => [
|
||||
'back' => $this->currentRoute,
|
||||
'route' => $route,
|
||||
'parameters' => $parameters
|
||||
]]);
|
||||
return redirect()->route($route);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ class Button extends Component
|
||||
public bool $disabled = false,
|
||||
public bool $isModal = false,
|
||||
public bool $noStyle = false,
|
||||
public string|null $modalId = null,
|
||||
public ?string $modalId = null,
|
||||
public string $defaultClass = "btn btn-primary btn-sm font-normal text-white normal-case no-animation rounded border-none"
|
||||
) {
|
||||
if ($this->noStyle) {
|
||||
@@ -23,9 +23,6 @@ class Button extends Component
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.forms.button');
|
||||
|
||||
@@ -17,7 +17,7 @@ class Checkbox extends Component
|
||||
public string|null $value = null,
|
||||
public string|null $label = null,
|
||||
public string|null $helper = null,
|
||||
public bool $instantSave = false,
|
||||
public string|bool $instantSave = false,
|
||||
public bool $disabled = false,
|
||||
public string $defaultClass = "toggle toggle-xs toggle-warning rounded disabled:bg-coolgray-200 disabled:opacity-50 placeholder:text-neutral-700"
|
||||
) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -58,9 +58,11 @@ function format_docker_envs_to_json($rawOutput)
|
||||
}
|
||||
|
||||
function getApplicationContainerStatus(Application $application) {
|
||||
$server = $application->destination->server;
|
||||
$server = data_get($application,'destination.server');
|
||||
$id = $application->id;
|
||||
|
||||
if (!$server) {
|
||||
return 'exited';
|
||||
}
|
||||
$containers = getCurrentApplicationContainerStatus($server, $id);
|
||||
if ($containers->count() > 0) {
|
||||
$status = data_get($containers[0], 'State', 'exited');
|
||||
@@ -84,7 +86,7 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
|
||||
|
||||
function generateApplicationContainerName(string $uuid, int $pull_request_id = 0)
|
||||
{
|
||||
$now = now()->format('YmdHis');
|
||||
$now = now()->format('Hisu');
|
||||
if ($pull_request_id !== 0 && $pull_request_id !== null) {
|
||||
return $uuid . '-pr-' . $pull_request_id . '-' . $now;
|
||||
} else {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -16,11 +16,6 @@ use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Sleep;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
/**
|
||||
* Run a Remote Process, which SSH's asynchronously into a machine to run the command(s).
|
||||
* @TODO Change 'root' to 'coolify' when it's able to run Docker commands without sudo
|
||||
*
|
||||
*/
|
||||
function remote_process(
|
||||
array $command,
|
||||
Server $server,
|
||||
@@ -82,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 '
|
||||
@@ -97,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);
|
||||
@@ -167,17 +174,23 @@ function validateServer(Server $server)
|
||||
{
|
||||
try {
|
||||
refresh_server_connection($server->privateKey);
|
||||
$uptime = instant_remote_process(['uptime'], $server);
|
||||
$uptime = instant_remote_process(['uptime'], $server, false);
|
||||
if (!$uptime) {
|
||||
$uptime = 'Server not reachable.';
|
||||
throw new \Exception('Server not reachable.');
|
||||
$server->settings->is_reachable = false;
|
||||
return [
|
||||
"uptime" => null,
|
||||
"dockerVersion" => null,
|
||||
];
|
||||
}
|
||||
$server->settings->is_reachable = true;
|
||||
|
||||
$dockerVersion = instant_remote_process(['docker version|head -2|grep -i version'], $server, false);
|
||||
if (!$dockerVersion) {
|
||||
$dockerVersion = 'Not installed.';
|
||||
throw new \Exception('Docker not installed.');
|
||||
$dockerVersion = null;
|
||||
return [
|
||||
"uptime" => $uptime,
|
||||
"dockerVersion" => null,
|
||||
];
|
||||
}
|
||||
$server->settings->is_usable = true;
|
||||
return [
|
||||
@@ -215,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,6 +2,9 @@
|
||||
|
||||
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;
|
||||
@@ -25,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
|
||||
{
|
||||
@@ -52,12 +59,13 @@ function showBoarding(): bool
|
||||
}
|
||||
function refreshSession(): void
|
||||
{
|
||||
$team = currentTeam();
|
||||
$team = Team::find(currentTeam()->id);
|
||||
session(['currentTeam' => $team]);
|
||||
}
|
||||
function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed
|
||||
{
|
||||
try {
|
||||
ray($err);
|
||||
ray('ERROR OCCURRED: ' . $err->getMessage());
|
||||
if ($err instanceof QueryException) {
|
||||
if ($err->errorInfo[0] === '23505') {
|
||||
@@ -70,6 +78,9 @@ function general_error_handler(Throwable | null $err = null, $that = null, $isJs
|
||||
} elseif ($err instanceof TooManyRequestsException) {
|
||||
throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds.");
|
||||
} else {
|
||||
if ($err->getMessage() === 'This action is unauthorized.') {
|
||||
return redirect()->route('dashboard')->with('error', $customErrorMessage ?? $err->getMessage());
|
||||
}
|
||||
throw new Exception($customErrorMessage ?? $err->getMessage());
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
@@ -117,10 +128,11 @@ function generateSSHKey()
|
||||
$key = RSA::createKey();
|
||||
return [
|
||||
'private' => $key->toString('PKCS1'),
|
||||
'public' => $key->getPublicKey()->toString('OpenSSH',['comment' => 'coolify-generated-ssh-key'])
|
||||
'public' => $key->getPublicKey()->toString('OpenSSH', ['comment' => 'coolify-generated-ssh-key'])
|
||||
];
|
||||
}
|
||||
function formatPrivateKey(string $privateKey) {
|
||||
function formatPrivateKey(string $privateKey)
|
||||
{
|
||||
$privateKey = trim($privateKey);
|
||||
if (!str_ends_with($privateKey, "\n")) {
|
||||
$privateKey .= "\n";
|
||||
@@ -135,30 +147,36 @@ function generate_application_name(string $git_repository, string $git_branch):
|
||||
|
||||
function is_transactional_emails_active(): bool
|
||||
{
|
||||
return data_get(InstanceSettings::get(), 'smtp_enabled');
|
||||
return isEmailEnabled(InstanceSettings::get());
|
||||
}
|
||||
|
||||
function set_transanctional_email_settings(InstanceSettings | null $settings = null): void
|
||||
function set_transanctional_email_settings(InstanceSettings | null $settings = null): string|null
|
||||
{
|
||||
if (!$settings) {
|
||||
$settings = InstanceSettings::get();
|
||||
}
|
||||
$password = data_get($settings, 'smtp_password');
|
||||
if (isset($password)) {
|
||||
$password = decrypt($password);
|
||||
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'));
|
||||
return 'resend';
|
||||
}
|
||||
|
||||
config()->set('mail.default', 'smtp');
|
||||
config()->set('mail.mailers.smtp', [
|
||||
"transport" => "smtp",
|
||||
"host" => data_get($settings, 'smtp_host'),
|
||||
"port" => data_get($settings, 'smtp_port'),
|
||||
"encryption" => data_get($settings, 'smtp_encryption'),
|
||||
"username" => data_get($settings, 'smtp_username'),
|
||||
"password" => $password,
|
||||
"timeout" => data_get($settings, 'smtp_timeout'),
|
||||
"local_domain" => null,
|
||||
]);
|
||||
if (data_get($settings, 'smtp_enabled')) {
|
||||
config()->set('mail.default', 'smtp');
|
||||
config()->set('mail.mailers.smtp', [
|
||||
"transport" => "smtp",
|
||||
"host" => data_get($settings, 'smtp_host'),
|
||||
"port" => data_get($settings, 'smtp_port'),
|
||||
"encryption" => data_get($settings, 'smtp_encryption'),
|
||||
"username" => data_get($settings, 'smtp_username'),
|
||||
"password" => data_get($settings, 'smtp_password'),
|
||||
"timeout" => data_get($settings, 'smtp_timeout'),
|
||||
"local_domain" => null,
|
||||
]);
|
||||
return 'smtp';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function base_ip(): string
|
||||
@@ -212,7 +230,7 @@ function isDev(): bool
|
||||
return config('app.env') === 'local';
|
||||
}
|
||||
|
||||
function is_cloud(): bool
|
||||
function isCloud(): bool
|
||||
{
|
||||
return !config('coolify.self_hosted');
|
||||
}
|
||||
@@ -231,9 +249,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());
|
||||
}
|
||||
@@ -241,17 +259,40 @@ function send_internal_notification(string $message): void
|
||||
function send_user_an_email(MailMessage $mail, string $email): void
|
||||
{
|
||||
$settings = InstanceSettings::get();
|
||||
set_transanctional_email_settings($settings);
|
||||
$type = set_transanctional_email_settings($settings);
|
||||
if (!$type) {
|
||||
throw new Exception('No email settings found.');
|
||||
}
|
||||
Mail::send(
|
||||
[],
|
||||
[],
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ function getEndDate()
|
||||
|
||||
function isSubscriptionActive()
|
||||
{
|
||||
if (!isCloud()) {
|
||||
return false;
|
||||
}
|
||||
$team = currentTeam();
|
||||
if (!$team) {
|
||||
return false;
|
||||
@@ -56,21 +59,19 @@ function isSubscriptionActive()
|
||||
if (!$subscription) {
|
||||
return false;
|
||||
}
|
||||
if (config('subscription.provider') === 'lemon') {
|
||||
if (isLemon()) {
|
||||
return $subscription->lemon_status === 'active';
|
||||
}
|
||||
if (config('subscription.provider') === 'stripe') {
|
||||
// if (isPaddle()) {
|
||||
// return $subscription->paddle_status === 'active';
|
||||
// }
|
||||
if (isStripe()) {
|
||||
return $subscription->stripe_invoice_paid === true && $subscription->stripe_cancel_at_period_end === false;
|
||||
}
|
||||
return false;
|
||||
// if (config('subscription.provider') === 'paddle') {
|
||||
// return $subscription->paddle_status === 'active';
|
||||
// }
|
||||
|
||||
}
|
||||
function isSubscriptionOnGracePeriod()
|
||||
{
|
||||
|
||||
$team = currentTeam();
|
||||
if (!$team) {
|
||||
return false;
|
||||
@@ -79,12 +80,12 @@ function isSubscriptionOnGracePeriod()
|
||||
if (!$subscription) {
|
||||
return false;
|
||||
}
|
||||
if (config('subscription.provider') === 'lemon') {
|
||||
if (isLemon()) {
|
||||
$is_still_grace_period = $subscription->lemon_ends_at &&
|
||||
Carbon::parse($subscription->lemon_ends_at) > Carbon::now();
|
||||
return $is_still_grace_period;
|
||||
}
|
||||
if (config('subscription.provider') === 'stripe') {
|
||||
if (isStripe()) {
|
||||
return $subscription->stripe_cancel_at_period_end;
|
||||
}
|
||||
return false;
|
||||
@@ -93,10 +94,22 @@ function subscriptionProvider()
|
||||
{
|
||||
return config('subscription.provider');
|
||||
}
|
||||
function isLemon()
|
||||
{
|
||||
return config('subscription.provider') === 'lemon';
|
||||
}
|
||||
function isStripe()
|
||||
{
|
||||
return config('subscription.provider') === 'stripe';
|
||||
}
|
||||
function isPaddle()
|
||||
{
|
||||
return config('subscription.provider') === 'paddle';
|
||||
}
|
||||
function getStripeCustomerPortalSession(Team $team)
|
||||
{
|
||||
Stripe::setApiKey(config('subscription.stripe_api_key'));
|
||||
$return_url = route('team.show');
|
||||
$return_url = route('team.index');
|
||||
$stripe_customer_id = $team->subscription->stripe_customer_id;
|
||||
$session = \Stripe\BillingPortal\Session::create([
|
||||
'customer' => $stripe_customer_id,
|
||||
@@ -124,6 +137,6 @@ function allowedPathsForBoardingAccounts()
|
||||
return [
|
||||
...allowedPathsForUnsubscribedAccounts(),
|
||||
'boarding',
|
||||
'livewire/message/boarding',
|
||||
'livewire/message/boarding.index',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"laravel/fortify": "^v1.16.0",
|
||||
"laravel/framework": "^v10.7.1",
|
||||
"laravel/horizon": "^5.15",
|
||||
"laravel/prompts": "^0.1.6",
|
||||
"laravel/sanctum": "^v3.2.1",
|
||||
"laravel/tinker": "^v2.8.1",
|
||||
"laravel/ui": "^4.2",
|
||||
@@ -42,7 +43,7 @@
|
||||
"laravel/pint": "^v1.8.0",
|
||||
"mockery/mockery": "^1.5.1",
|
||||
"nunomaduro/collision": "^v7.4.0",
|
||||
"pestphp/pest": "^v2.4.0",
|
||||
"pestphp/pest": "^2.16",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^10.0.19",
|
||||
"serversideup/spin": "^v1.1.0",
|
||||
|
||||
142
composer.lock
generated
142
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "da14dce99d76abcaaa6393166eda049a",
|
||||
"content-hash": "0603276b60e77cd859fabacdaaf31550",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
@@ -6654,16 +6654,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v6.3.2",
|
||||
"version": "v6.3.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "aa5d64ad3f63f2e48964fc81ee45cb318a723898"
|
||||
"reference": "eca495f2ee845130855ddf1cf18460c38966c8b6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/aa5d64ad3f63f2e48964fc81ee45cb318a723898",
|
||||
"reference": "aa5d64ad3f63f2e48964fc81ee45cb318a723898",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/eca495f2ee845130855ddf1cf18460c38966c8b6",
|
||||
"reference": "eca495f2ee845130855ddf1cf18460c38966c8b6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6724,7 +6724,7 @@
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v6.3.2"
|
||||
"source": "https://github.com/symfony/console/tree/v6.3.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -6740,7 +6740,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-07-19T20:17:28+00:00"
|
||||
"time": "2023-08-16T10:10:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/css-selector",
|
||||
@@ -7761,16 +7761,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
|
||||
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
|
||||
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
|
||||
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7785,7 +7785,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
@@ -7823,7 +7823,7 @@
|
||||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -7839,7 +7839,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
"time": "2023-01-26T09:26:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-iconv",
|
||||
@@ -7926,16 +7926,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||
"reference": "511a08c03c1960e08a883f4cffcacd219b758354"
|
||||
"reference": "875e90aeea2777b6f135677f618529449334a612"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354",
|
||||
"reference": "511a08c03c1960e08a883f4cffcacd219b758354",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612",
|
||||
"reference": "875e90aeea2777b6f135677f618529449334a612",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -7947,7 +7947,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
@@ -7987,7 +7987,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8003,7 +8003,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
"time": "2023-01-26T09:26:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-idn",
|
||||
@@ -8094,16 +8094,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
"reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6"
|
||||
"reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6",
|
||||
"reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
|
||||
"reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8115,7 +8115,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
@@ -8158,7 +8158,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8174,20 +8174,20 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
"time": "2023-01-26T09:26:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
|
||||
"reference": "42292d99c55abe617799667f454222c54c60e229"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
||||
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
|
||||
"reference": "42292d99c55abe617799667f454222c54c60e229",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8202,7 +8202,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
@@ -8241,7 +8241,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8257,7 +8257,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
"time": "2023-07-28T09:04:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php72",
|
||||
@@ -8337,16 +8337,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
|
||||
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
|
||||
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
|
||||
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8355,7 +8355,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
@@ -8400,7 +8400,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8416,7 +8416,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
"time": "2023-01-26T09:26:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php83",
|
||||
@@ -8579,16 +8579,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v6.3.2",
|
||||
"version": "v6.3.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d"
|
||||
"reference": "0b5c29118f2e980d455d2e34a5659f4579847c54"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d",
|
||||
"reference": "c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54",
|
||||
"reference": "0b5c29118f2e980d455d2e34a5659f4579847c54",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -8620,7 +8620,7 @@
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v6.3.2"
|
||||
"source": "https://github.com/symfony/process/tree/v6.3.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -8636,7 +8636,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-07-12T16:00:22+00:00"
|
||||
"time": "2023-08-07T10:39:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/psr-http-message-bridge",
|
||||
@@ -9982,16 +9982,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
"version": "v7.2.5",
|
||||
"version": "v7.2.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paratestphp/paratest.git",
|
||||
"reference": "4d7ad5b6564f63baa1b948ecad05439f22880942"
|
||||
"reference": "7f372b5bb59b4271adedc67d3129df29b84c4173"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/4d7ad5b6564f63baa1b948ecad05439f22880942",
|
||||
"reference": "4d7ad5b6564f63baa1b948ecad05439f22880942",
|
||||
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/7f372b5bb59b4271adedc67d3129df29b84c4173",
|
||||
"reference": "7f372b5bb59b4271adedc67d3129df29b84c4173",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -10005,19 +10005,19 @@
|
||||
"phpunit/php-code-coverage": "^10.1.3",
|
||||
"phpunit/php-file-iterator": "^4.0.2",
|
||||
"phpunit/php-timer": "^6.0",
|
||||
"phpunit/phpunit": "^10.3.1",
|
||||
"phpunit/phpunit": "^10.3.2",
|
||||
"sebastian/environment": "^6.0.1",
|
||||
"symfony/console": "^6.3.2",
|
||||
"symfony/process": "^6.3.2"
|
||||
"symfony/console": "^6.3.4",
|
||||
"symfony/process": "^6.3.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^12.0.0",
|
||||
"ext-pcov": "*",
|
||||
"ext-posix": "*",
|
||||
"infection/infection": "^0.27.0",
|
||||
"phpstan/phpstan": "^1.10.26",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.1.3",
|
||||
"phpstan/phpstan-phpunit": "^1.3.13",
|
||||
"phpstan/phpstan": "^1.10.32",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.1.4",
|
||||
"phpstan/phpstan-phpunit": "^1.3.14",
|
||||
"phpstan/phpstan-strict-rules": "^1.5.1",
|
||||
"squizlabs/php_codesniffer": "^3.7.2",
|
||||
"symfony/filesystem": "^6.3.1"
|
||||
@@ -10061,7 +10061,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/paratestphp/paratest/issues",
|
||||
"source": "https://github.com/paratestphp/paratest/tree/v7.2.5"
|
||||
"source": "https://github.com/paratestphp/paratest/tree/v7.2.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -10073,7 +10073,7 @@
|
||||
"type": "paypal"
|
||||
}
|
||||
],
|
||||
"time": "2023-08-08T13:23:59+00:00"
|
||||
"time": "2023-08-29T07:47:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fakerphp/faker",
|
||||
@@ -10707,24 +10707,24 @@
|
||||
},
|
||||
{
|
||||
"name": "pestphp/pest",
|
||||
"version": "v2.16.0",
|
||||
"version": "v2.16.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pestphp/pest.git",
|
||||
"reference": "cbd6a650576714c673dbb0575989663f7f5c8b6d"
|
||||
"reference": "55b92666482b7d4320b7869c4eea7333d35c5631"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/pestphp/pest/zipball/cbd6a650576714c673dbb0575989663f7f5c8b6d",
|
||||
"reference": "cbd6a650576714c673dbb0575989663f7f5c8b6d",
|
||||
"url": "https://api.github.com/repos/pestphp/pest/zipball/55b92666482b7d4320b7869c4eea7333d35c5631",
|
||||
"reference": "55b92666482b7d4320b7869c4eea7333d35c5631",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"brianium/paratest": "^7.2.5",
|
||||
"brianium/paratest": "^7.2.6",
|
||||
"nunomaduro/collision": "^7.8.1",
|
||||
"nunomaduro/termwind": "^1.15.1",
|
||||
"pestphp/pest-plugin": "^2.0.1",
|
||||
"pestphp/pest-plugin-arch": "^2.3.1",
|
||||
"pestphp/pest-plugin": "^2.1.1",
|
||||
"pestphp/pest-plugin-arch": "^2.3.3",
|
||||
"php": "^8.1.0",
|
||||
"phpunit/phpunit": "^10.3.2"
|
||||
},
|
||||
@@ -10734,8 +10734,8 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"pestphp/pest-dev-tools": "^2.16.0",
|
||||
"pestphp/pest-plugin-type-coverage": "^2.0.0",
|
||||
"symfony/process": "^6.3.2"
|
||||
"pestphp/pest-plugin-type-coverage": "^2.2.0",
|
||||
"symfony/process": "^6.3.4"
|
||||
},
|
||||
"bin": [
|
||||
"bin/pest"
|
||||
@@ -10793,7 +10793,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/pestphp/pest/issues",
|
||||
"source": "https://github.com/pestphp/pest/tree/v2.16.0"
|
||||
"source": "https://github.com/pestphp/pest/tree/v2.16.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -10805,7 +10805,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-08-21T08:42:07+00:00"
|
||||
"time": "2023-08-29T09:30:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pestphp/pest-plugin",
|
||||
|
||||
@@ -17,7 +17,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
'name' => env('APP_NAME', 'Coolify'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -94,7 +94,7 @@ return [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => 'password_reset_tokens',
|
||||
'expire' => 60,
|
||||
'expire' => 10,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user