diff --git a/.dockerignore b/.dockerignore
index 2eba3cb46..0adca0b32 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -24,3 +24,4 @@ yarn-error.log
/.ssh
.ignition.json
.env.dusk.local
+docker/coolify-realtime/node_modules
diff --git a/.gitattributes b/.gitattributes
index fcb21d396..c48a5898b 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -8,4 +8,4 @@
/.github export-ignore
CHANGELOG.md export-ignore
-.styleci.yml export-ignore
+.styleci.yml export-ignore
\ No newline at end of file
diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml
index 7017b4897..5271143ec 100644
--- a/.github/workflows/coolify-production-build.yml
+++ b/.github/workflows/coolify-production-build.yml
@@ -11,7 +11,7 @@ on:
- docker/coolify-helper/Dockerfile
- docker/coolify-realtime/Dockerfile
- docker/testing-host/Dockerfile
- - templates/service-templates.json
+ - templates/**
env:
GITHUB_REGISTRY: ghcr.io
@@ -41,7 +41,7 @@ jobs:
- name: Get Version
id: version
run: |
- echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT]
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
@@ -76,7 +76,7 @@ jobs:
- name: Get Version
id: version
run: |
- echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT]
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml
index 7e937d17a..ef247170f 100644
--- a/.github/workflows/coolify-realtime-next.yml
+++ b/.github/workflows/coolify-realtime-next.yml
@@ -8,6 +8,7 @@ on:
- docker/coolify-realtime/Dockerfile
- docker/coolify-realtime/terminal-server.js
- docker/coolify-realtime/package.json
+ - docker/coolify-realtime/package-lock.json
- docker/coolify-realtime/soketi-entrypoint.sh
env:
diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml
index 97bfd52eb..9654a21b0 100644
--- a/.github/workflows/coolify-realtime.yml
+++ b/.github/workflows/coolify-realtime.yml
@@ -8,6 +8,7 @@ on:
- docker/coolify-realtime/Dockerfile
- docker/coolify-realtime/terminal-server.js
- docker/coolify-realtime/package.json
+ - docker/coolify-realtime/package-lock.json
- docker/coolify-realtime/soketi-entrypoint.sh
env:
diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml
index 6e4d4adc3..2c57a36a3 100644
--- a/.github/workflows/coolify-staging-build.yml
+++ b/.github/workflows/coolify-staging-build.yml
@@ -11,7 +11,7 @@ on:
- docker/coolify-helper/Dockerfile
- docker/coolify-realtime/Dockerfile
- docker/testing-host/Dockerfile
- - templates/service-templates.json
+ - templates/**
env:
GITHUB_REGISTRY: ghcr.io
diff --git a/.gitignore b/.gitignore
index 1a021ab3e..d7ee7e96c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,5 @@ _ide_helper_models.php
scripts/load-test/*
.ignition.json
.env.dusk.local
+docker/coolify-realtime/node_modules
+.DS_Store
diff --git a/.gitpod.yml b/.gitpod.yml
deleted file mode 100644
index 6fd6797b5..000000000
--- a/.gitpod.yml
+++ /dev/null
@@ -1,65 +0,0 @@
-tasks:
- - name: Setup Spin environment and Composer dependencies
- # Fix because of https://github.com/gitpod-io/gitpod/issues/16614
- before: sudo curl -o /usr/local/bin/docker-compose -fsSL https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-$(uname -m)
- init: |
- cp .env.development.example .env &&
- sed -i "s#APP_URL=http://localhost#APP_URL=$(gp url 8000)#g" .env
- sed -i "s#USERID=#USERID=33333#g" .env
- sed -i "s#GROUPID=#GROUPID=33333#g" .env
- composer install --ignore-platform-reqs
- ./vendor/bin/spin up -d
- ./vendor/bin/spin exec -u webuser coolify php artisan key:generate
- ./vendor/bin/spin exec -u webuser coolify php artisan storage:link
- ./vendor/bin/spin exec -u webuser coolify php artisan migrate:fresh --seed
- cat .coolify-logo
- gp sync-done spin-is-ready
-
- - name: Install Node dependencies and run Vite
- command: |
- echo "Waiting for Sail environment to boot up."
- gp sync-await spin-is-ready
- ./vendor/bin/spin exec vite npm install
- ./vendor/bin/spin exec vite npm run dev -- --host
-
- - name: Laravel Queue Worker, listening to code changes
- command: |
- echo "Waiting for Sail environment to boot up."
- gp sync-await spin-is-ready
- ./vendor/bin/spin exec -u webuser coolify php artisan queue:listen
-
-ports:
- - port: 5432
- onOpen: ignore
- name: PostgreSQL
- visibility: public
- - port: 5173
- onOpen: ignore
- visibility: public
- name: Node Server for Vite
- - port: 8000
- onOpen: ignore
- visibility: public
- name: Coolify
-
-# Configure vscode
-vscode:
- extensions:
- - bmewburn.vscode-intelephense-client
- - ikappas.composer
- - ms-azuretools.vscode-docker
- - ecmel.vscode-html-css
- - MehediDracula.php-namespace-resolver
- - wmaurer.change-case
- - Equinusocio.vsc-community-material-theme
- - EditorConfig.EditorConfig
- - streetsidesoftware.code-spell-checker
- - rangav.vscode-thunder-client
- - PKief.material-icon-theme
- - cierra.livewire-vscode
- - lennardv.livewire-goto-updated
- - bradlc.vscode-tailwindcss
- - heybourn.headwind
- - adrianwilczynski.alpine-js-intellisense
- - amiralizadeh9480.laravel-extra-intellisense
- - shufo.vscode-blade-formatter
diff --git a/README.md b/README.md
index 14a741088..dac48d127 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
# About the Project
-Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
+Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
It helps you manage your servers, applications, and databases on your own hardware; you only need an SSH connection. You can manage VPS, Bare Metal, Raspberry PIs, and anything else.
@@ -22,6 +22,9 @@ curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
```
You can find the installation script source [here](./scripts/install.sh).
+> [!NOTE]
+> Please refer to the [docs](https://coolify.io/docs/installation) for more information about the installation.
+
# Support
Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact).
@@ -37,21 +40,21 @@ Special thanks to our biggest sponsors!
### Special Sponsors
-
+
* [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry.
* [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions.
* [Logto](https://logto.io/?ref=coolify) - An open-source authentication and authorization solution for building secure login systems and managing user identities.
+* [Tolgee](https://tolgee.io/?ref=coolify) - Developer & translator friendly web-based localization platform.
* [BC Direct](https://bc.direct/?ref=coolify.io) - A digital marketing agency specializing in e-commerce solutions and online business growth strategies.
* [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution.
* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks.
* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase.
+* [GoldenVM](https://billing.goldenvm.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management.
-* [Fractal Networks](https://fractalnetworks.co/?ref=coolify.io) - A decentralized network infrastructure company focusing on secure and private communication solutions.
* [Advin](https://coolify.ad.vin/?ref=coolify.io) - A digital advertising agency specializing in programmatic advertising and data-driven marketing strategies.
* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets.
* [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers.
-* [Latitude](https://latitude.sh/?ref=coolify.io) - A cloud computing platform offering bare metal servers and cloud instances for developers and businesses.
* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities.
* [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries.
* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools.
@@ -60,6 +63,7 @@ Special thanks to our biggest sponsors!
* [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and digital marketing services for businesses.
* [Saasykit](https://saasykit.com/?ref=coolify.io) - A Laravel-based boilerplate providing essential components and features for building SaaS applications quickly.
* [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
+* [LiquidWeb](https://liquidweb.com/?utm_source=coolify.io) - Fast web hosting provider.
## Github Sponsors ($40+)
@@ -87,7 +91,11 @@ Special thanks to our biggest sponsors!
-
+
+
+
+
+
## Organizations
@@ -121,7 +129,6 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
- Better support
- Less maintenance for you
-
# Recognitions
@@ -138,6 +145,13 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
+# Core Maintainers
+
+| Andras Bacsai | 🏔️ Peak |
+|------------|------------|
+|
|
|
+|
|
|
+
# Repo Activity

diff --git a/RELEASE.md b/RELEASE.md
index d9f05f17d..bc159b040 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,6 +1,6 @@
# Coolify Release Guide
-This guide outlines the release process for Coolify, intended for developers and those interested in understanding how releases are managed and deployed.
+This guide outlines the release process for Coolify, intended for developers and those interested in understanding how Coolify releases are managed and deployed.
## Table of Contents
- [Release Process](#release-process)
@@ -19,19 +19,19 @@ This guide outlines the release process for Coolify, intended for developers and
- Improvements, fixes, and new features are developed on the `next` branch or separate feature branches.
2. **Merging to `main`**
- - Once ready, changes are merged from the `next` branch into the `main` branch.
+ - Once ready, changes are merged from the `next` branch into the `main` branch (via a pull request).
3. **Building the Release**
- - After merging to `main`, GitHub Actions automatically builds release images for all architectures and pushes them to the GitHub Container Registry with the version tag and the `latest` tag.
+ - After merging to `main`, GitHub Actions automatically builds release images for all architectures and pushes them to the GitHub Container Registry and Docker Hub with the specific version tag and the `latest` tag.
4. **Creating a GitHub Release**
- A new GitHub release is manually created with details of the changes made in the version.
5. **Updating the CDN**
- - To make a new version publicly available, the version information on the CDN needs to be updated: [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json)
+ - To make a new version publicly available, the version information on the CDN needs to be updated manually. After that the new version number will be available at [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json).
> [!NOTE]
-> The CDN update may not occur immediately after the GitHub release. It can take hours or even days due to additional testing, stability checks, or potential hotfixes. **The update becomes available only after the CDN is updated.**
+> The CDN update may not occur immediately after the GitHub release. It can take hours or even days due to additional testing, stability checks, or potential hotfixes. **The update becomes available only after the CDN is updated. After the CDN is updated, a discord announcement will be made in the Production Release channel.**
## Version Types
@@ -39,10 +39,10 @@ This guide outlines the release process for Coolify, intended for developers and
Stable (coming soon)
- **Stable**
- - The production version suitable for stable, production environments (generally recommended).
- - **Update Frequency:** Every 2 to 4 weeks, with more frequent possible hotfixes.
+ - The production version suitable for stable, production environments (recommended).
+ - **Update Frequency:** Every 2 to 4 weeks, with more frequent possible fixes.
- **Release Size:** Larger but less frequent releases. Multiple nightly versions are consolidated into a single stable release.
- - **Versioning Scheme:** Follows semantic versioning (e.g., `v4.0.0`).
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `v4.0.0`, `4.1.0`, etc.).
- **Installation Command:**
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
@@ -57,7 +57,7 @@ This guide outlines the release process for Coolify, intended for developers and
- The latest development version, suitable for testing the latest changes and experimenting with new features.
- **Update Frequency:** Daily or bi-weekly updates.
- **Release Size:** Smaller, more frequent releases.
- - **Versioning Scheme:** TO BE DETERMINED
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-nightly.1`, `4.1.0-nightly.2`, etc.).
- **Installation Command:**
```bash
curl -fsSL https://cdn.coollabs.io/coolify-nightly/install.sh | bash -s next
@@ -73,11 +73,11 @@ This guide outlines the release process for Coolify, intended for developers and
- **Purpose:** Allows users to test and provide feedback on new features and changes before they become stable.
- **Update Frequency:** Available if we think beta testing is necessary.
- **Release Size:** Same size as stable release as it will become the next stabe release after some time.
- - **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-beta.1`).
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-beta.1`, `4.1.0-beta.2`, etc.).
- **Installation Command:**
- ```bash
+ ```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
- ```
+ ```
@@ -117,12 +117,15 @@ When a new version is released and a new GitHub release is created, it doesn't i
> [!IMPORTANT]
> The cloud version of Coolify may be several versions behind the latest GitHub releases even if the CDN is updated. This is intentional to ensure stability and reliability for cloud users and Andras will manully update the cloud version when the update is ready.
-## Manually Update to Specific Versions
+## Manually Update/ Downgrade to Specific Versions
> [!CAUTION]
-> Updating to unreleased versions is not recommended and may cause issues. Use at your own risk!
+> Updating to unreleased versions is not recommended and can cause issues.
-To update your Coolify instance to a specific (unreleased) version, use the following command:
+> [!IMPORTANT]
+> Downgrading is supported but not recommended and can cause issues because of database migrations and other changes.
+
+To update your Coolify instance to a specific version, use the following command:
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash -s
diff --git a/SECURITY.md b/SECURITY.md
index ad3a4addd..0711bf5b5 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,15 +2,24 @@
## Supported Versions
-Use this section to tell people about which versions of your project are
-currently being supported with security updates.
+Currently supported, maintained and updated versions:
-| Version | Supported |
-| ------- | ------------------ |
-| > 4 | :white_check_mark: |
-| 3 | :x: |
+| Version | Supported | Support Status |
+| ------- | ------------------ | -------------- |
+| 4.x | :white_check_mark: | Active Development & Security Updates |
+| < 4.0 | :x: | End of Life (no security updates) |
+## Security Updates
+
+We take security seriously. Security updates are released as soon as possible after a vulnerability is discovered and verified.
## Reporting a Vulnerability
-If you have any vulnerability please report at hi@coollabs.io
+If you discover a security vulnerability, please follow these steps:
+
+1. **DO NOT** disclose the vulnerability publicly.
+2. Send a detailed report to: `hi@coollabs.io`.
+3. Include in your report:
+ - A description of the vulnerability
+ - Steps to reproduce the issue
+ - Potential impact
diff --git a/app/Actions/Application/IsHorizonQueueEmpty.php b/app/Actions/Application/IsHorizonQueueEmpty.php
new file mode 100644
index 000000000..17966b8a0
--- /dev/null
+++ b/app/Actions/Application/IsHorizonQueueEmpty.php
@@ -0,0 +1,37 @@
+getRecent();
+ if ($recent) {
+ $running = $recent->filter(function ($job) use ($hostname) {
+ $payload = json_decode($job->payload);
+ $tags = data_get($payload, 'tags');
+
+ return $job->status != 'completed' &&
+ $job->status != 'failed' &&
+ isset($tags) &&
+ is_array($tags) &&
+ in_array('server:'.$hostname, $tags);
+ });
+ if ($running->count() > 0) {
+ echo 'false';
+
+ return false;
+ }
+ }
+ echo 'true';
+
+ return true;
+ }
+}
diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php
index cab7e45f0..642b4ba45 100644
--- a/app/Actions/Application/StopApplication.php
+++ b/app/Actions/Application/StopApplication.php
@@ -10,6 +10,8 @@ class StopApplication
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
{
try {
diff --git a/app/Actions/CoolifyTask/PrepareCoolifyTask.php b/app/Actions/CoolifyTask/PrepareCoolifyTask.php
index 6676b7937..3f76a2e3c 100644
--- a/app/Actions/CoolifyTask/PrepareCoolifyTask.php
+++ b/app/Actions/CoolifyTask/PrepareCoolifyTask.php
@@ -3,7 +3,6 @@
namespace App\Actions\CoolifyTask;
use App\Data\CoolifyTaskArgs;
-use App\Enums\ActivityTypes;
use App\Jobs\CoolifyTask;
use Spatie\Activitylog\Models\Activity;
@@ -47,11 +46,7 @@ class PrepareCoolifyTask
call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish,
call_event_data: $this->remoteProcessArgs->call_event_data,
);
- if ($this->remoteProcessArgs->type === ActivityTypes::COMMAND->value) {
- dispatch($job)->onQueue('high');
- } else {
- dispatch($job);
- }
+ dispatch($job);
$this->activity->refresh();
return $this->activity;
diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php
index 51c5a1f9e..981b81378 100644
--- a/app/Actions/CoolifyTask/RunRemoteProcess.php
+++ b/app/Actions/CoolifyTask/RunRemoteProcess.php
@@ -9,6 +9,7 @@ use App\Jobs\ApplicationDeploymentJob;
use App\Models\Server;
use Illuminate\Process\ProcessResult;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Spatie\Activitylog\Models\Activity;
@@ -124,6 +125,7 @@ class RunRemoteProcess
]));
}
} catch (\Throwable $e) {
+ Log::error('Error calling event: '.$e->getMessage());
}
}
diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php
index 1e8616416..42c6e1449 100644
--- a/app/Actions/Database/StartClickhouse.php
+++ b/app/Actions/Database/StartClickhouse.php
@@ -24,7 +24,7 @@ class StartClickhouse
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -99,8 +99,8 @@ class StartClickhouse
}
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
diff --git a/app/Actions/Database/StartDatabase.php b/app/Actions/Database/StartDatabase.php
index 73b2f9ac0..e2fa6fc87 100644
--- a/app/Actions/Database/StartDatabase.php
+++ b/app/Actions/Database/StartDatabase.php
@@ -16,6 +16,8 @@ class StartDatabase
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
{
$server = $database->destination->server;
diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php
index d7a3bc697..3ddf6c036 100644
--- a/app/Actions/Database/StartDatabaseProxy.php
+++ b/app/Actions/Database/StartDatabaseProxy.php
@@ -18,6 +18,8 @@ class StartDatabaseProxy
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
{
$internalPort = null;
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index 6274737a1..ea235be4e 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -26,7 +26,7 @@ class StartDragonfly
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -96,8 +96,8 @@ class StartDragonfly
}
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index 734b28322..010bf5884 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -27,7 +27,7 @@ class StartKeydb
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -107,8 +107,8 @@ class StartKeydb
}
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index aa6b79126..2437a013e 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -24,7 +24,7 @@ class StartMariadb
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -101,8 +101,8 @@ class StartMariadb
}
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index 8d0a6b3ca..a33e72c27 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -25,8 +25,12 @@ class StartMongodb
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
+ if (isDev()) {
+ $this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name;
+ }
+
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -117,8 +121,8 @@ class StartMongodb
];
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index 0339501b0..0b19b3f0c 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -24,7 +24,7 @@ class StartMysql
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -101,8 +101,8 @@ class StartMysql
}
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index 5c956acbc..7faa232c3 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -25,7 +25,7 @@ class StartPostgresql
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/",
];
@@ -122,8 +122,8 @@ class StartPostgresql
];
}
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index e445a246e..bacf49f82 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -25,7 +25,7 @@ class StartRedis
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
@@ -110,8 +110,8 @@ class StartRedis
}
// Add custom docker run options
- $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
- $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+ $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
+ $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
diff --git a/app/Actions/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php
index 9e9a62170..9ee794351 100644
--- a/app/Actions/Database/StopDatabaseProxy.php
+++ b/app/Actions/Database/StopDatabaseProxy.php
@@ -2,7 +2,7 @@
namespace App\Actions\Database;
-use App\Events\DatabaseStatusChanged;
+use App\Events\DatabaseProxyStopped;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
@@ -18,6 +18,8 @@ class StopDatabaseProxy
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|ServiceDatabase|StandaloneDragonfly|StandaloneClickhouse $database)
{
$server = data_get($database, 'destination.server');
@@ -27,7 +29,11 @@ class StopDatabaseProxy
$server = data_get($database, 'service.server');
}
instant_remote_process(["docker rm -f {$uuid}-proxy"], $server);
+
+ $database->is_public = false;
$database->save();
- DatabaseStatusChanged::dispatch();
+
+ DatabaseProxyStopped::dispatch();
+
}
}
diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php
index 95c22efc1..706356930 100644
--- a/app/Actions/Docker/GetContainersStatus.php
+++ b/app/Actions/Docker/GetContainersStatus.php
@@ -7,7 +7,6 @@ use App\Actions\Shared\ComplexStatusCheck;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\ServiceDatabase;
-use App\Notifications\Container\ContainerRestarted;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -16,6 +15,8 @@ class GetContainersStatus
{
use AsAction;
+ public string $jobQueue = 'high';
+
public $applications;
public ?Collection $containers;
@@ -107,6 +108,8 @@ class GetContainersStatus
$statusFromDb = $preview->status;
if ($statusFromDb !== $containerStatus) {
$preview->update(['status' => $containerStatus]);
+ } else {
+ $preview->update(['last_online_at' => now()]);
}
} else {
//Notify user that this container should not be there.
@@ -118,6 +121,8 @@ class GetContainersStatus
$statusFromDb = $application->status;
if ($statusFromDb !== $containerStatus) {
$application->update(['status' => $containerStatus]);
+ } else {
+ $application->update(['last_online_at' => now()]);
}
} else {
//Notify user that this container should not be there.
@@ -160,7 +165,10 @@ class GetContainersStatus
$statusFromDb = $database->status;
if ($statusFromDb !== $containerStatus) {
$database->update(['status' => $containerStatus]);
+ } else {
+ $database->update(['last_online_at' => now()]);
}
+
if ($isPublic) {
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
@@ -171,7 +179,7 @@ class GetContainersStatus
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
- $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
+ // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server));
}
}
} else {
@@ -202,6 +210,8 @@ class GetContainersStatus
if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$service->update(['status' => $containerStatus]);
+ } else {
+ $service->update(['last_online_at' => now()]);
}
}
}
diff --git a/app/Actions/License/CheckResaleLicense.php b/app/Actions/License/CheckResaleLicense.php
deleted file mode 100644
index 26a1ff7bf..000000000
--- a/app/Actions/License/CheckResaleLicense.php
+++ /dev/null
@@ -1,66 +0,0 @@
-update([
- 'is_resale_license_active' => true,
- ]);
-
- return;
- }
- // if (!$settings->resale_license) {
- // return;
- // }
- $base_url = config('coolify.license_url');
- $instance_id = config('app.id');
- $data = Http::withHeaders([
- 'Accept' => 'application/json',
- ])->get("$base_url/lemon/validate", [
- 'license_key' => $settings->resale_license,
- 'instance_id' => $instance_id,
- ])->json();
- if (data_get($data, 'valid') === true && data_get($data, 'license_key.status') === 'active') {
- $settings->update([
- 'is_resale_license_active' => true,
- ]);
-
- return;
- }
- $data = Http::withHeaders([
- 'Accept' => 'application/json',
- ])->get("$base_url/lemon/activate", [
- 'license_key' => $settings->resale_license,
- 'instance_id' => $instance_id,
- ])->json();
- if (data_get($data, 'activated') === true) {
- $settings->update([
- 'is_resale_license_active' => true,
- ]);
-
- return;
- }
- if (data_get($data, 'license_key.status') === 'active') {
- throw new \Exception('Invalid license key.');
- }
- throw new \Exception('Cannot activate license key.');
- } catch (\Throwable $e) {
- $settings->update([
- 'resale_license' => null,
- 'is_resale_license_active' => false,
- ]);
- throw $e;
- }
- }
-}
diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php
index d64804758..6c8dd5234 100644
--- a/app/Actions/Proxy/CheckProxy.php
+++ b/app/Actions/Proxy/CheckProxy.php
@@ -4,6 +4,7 @@ namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Models\Server;
+use Illuminate\Support\Facades\Log;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -29,7 +30,7 @@ class CheckProxy
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
return false;
}
- ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
+ ['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
if (! $uptime) {
throw new \Exception($error);
}
@@ -88,6 +89,7 @@ class CheckProxy
$portsToCheck = [];
}
} catch (\Exception $e) {
+ Log::error('Error checking proxy: '.$e->getMessage());
}
if (count($portsToCheck) === 0) {
return false;
diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php
index 7c93720cb..9bc506d9b 100644
--- a/app/Actions/Proxy/StartProxy.php
+++ b/app/Actions/Proxy/StartProxy.php
@@ -2,6 +2,7 @@
namespace App\Actions\Proxy;
+use App\Enums\ProxyTypes;
use App\Events\ProxyStarted;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -37,11 +38,16 @@ class StartProxy
"echo 'Successfully started coolify-proxy.'",
]);
} else {
- $caddfile = 'import /dynamic/*.caddy';
+ if (isDev()) {
+ if ($proxyType === ProxyTypes::CADDY->value) {
+ $proxy_path = '/data/coolify/proxy/caddy';
+ }
+ }
+ $caddyfile = 'import /dynamic/*.caddy';
$commands = $commands->merge([
"mkdir -p $proxy_path/dynamic",
"cd $proxy_path",
- "echo '$caddfile' > $proxy_path/dynamic/Caddyfile",
+ "echo '$caddyfile' > $proxy_path/dynamic/Caddyfile",
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',
diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php
index dc6ac12bf..0349ead89 100644
--- a/app/Actions/Server/CleanupDocker.php
+++ b/app/Actions/Server/CleanupDocker.php
@@ -9,11 +9,13 @@ class CleanupDocker
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Server $server)
{
$settings = instanceSettings();
$helperImageVersion = data_get($settings, 'helper_version');
- $helperImage = config('coolify.helper_image');
+ $helperImage = config('constants.coolify.helper_image');
$helperImageWithVersion = "$helperImage:$helperImageVersion";
$commands = [
diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php
index fd4dd150c..cbcb20368 100644
--- a/app/Actions/Server/InstallDocker.php
+++ b/app/Actions/Server/InstallDocker.php
@@ -12,11 +12,11 @@ class InstallDocker
public function handle(Server $server)
{
+ $dockerVersion = config('constants.docker.minimum_required_version');
$supported_os_type = $server->validateOS();
if (! $supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.');
}
- $dockerVersion = '26.0';
$config = base64_encode('{
"log-driver": "json-file",
"log-opts": {
diff --git a/app/Actions/Server/ServerCheck.php b/app/Actions/Server/ServerCheck.php
index f61422807..75b8501f3 100644
--- a/app/Actions/Server/ServerCheck.php
+++ b/app/Actions/Server/ServerCheck.php
@@ -14,7 +14,7 @@ use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
-use Arr;
+use Illuminate\Support\Arr;
use Lorisleiva\Actions\Concerns\AsAction;
class ServerCheck
@@ -51,7 +51,6 @@ class ServerCheck
$containerReplicates = null;
$this->isSentinel = true;
-
} else {
['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers();
// ServerStorageCheckJob::dispatch($this->server);
@@ -148,7 +147,6 @@ class ServerCheck
} else {
$labels = Arr::undot(data_get($container, 'Config.Labels'));
}
-
}
$managed = data_get($labels, 'coolify.managed');
if (! $managed) {
@@ -259,7 +257,7 @@ class ServerCheck
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
- $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
+ // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server));
}
}
}
diff --git a/app/Actions/Server/StartLogDrain.php b/app/Actions/Server/StartLogDrain.php
index 0e8036cd9..0d28a0099 100644
--- a/app/Actions/Server/StartLogDrain.php
+++ b/app/Actions/Server/StartLogDrain.php
@@ -9,6 +9,8 @@ class StartLogDrain
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Server $server)
{
if ($server->settings->is_logdrain_newrelic_enabled) {
@@ -169,7 +171,7 @@ Files:
');
$license_key = $server->settings->logdrain_newrelic_license_key;
$base_uri = $server->settings->logdrain_newrelic_base_uri;
- $base_path = config('coolify.base_config_path');
+ $base_path = config('constants.coolify.base_config_path');
$config_path = $base_path.'/log-drains';
$fluent_bit_config = $config_path.'/fluent-bit.conf';
diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php
index 3185c22b7..be9b4062c 100644
--- a/app/Actions/Server/UpdateCoolify.php
+++ b/app/Actions/Server/UpdateCoolify.php
@@ -4,6 +4,7 @@ namespace App\Actions\Server;
use App\Jobs\PullHelperImageJob;
use App\Models\Server;
+use Illuminate\Support\Sleep;
use Lorisleiva\Actions\Concerns\AsAction;
class UpdateCoolify
@@ -18,14 +19,19 @@ class UpdateCoolify
public function handle($manual_update = false)
{
+ if (isDev()) {
+ Sleep::for(10)->seconds();
+
+ return;
+ }
$settings = instanceSettings();
$this->server = Server::find(0);
if (! $this->server) {
return;
}
- CleanupDocker::dispatch($this->server)->onQueue('high');
+ CleanupDocker::dispatch($this->server);
$this->latestVersion = get_latest_version_of_coolify();
- $this->currentVersion = config('version');
+ $this->currentVersion = config('constants.coolify.version');
if (! $manual_update) {
if (! $settings->is_auto_update_enabled) {
return;
@@ -44,19 +50,7 @@ class UpdateCoolify
private function update()
{
- if (isDev()) {
- remote_process([
- 'sleep 10',
- ], $this->server);
-
- return;
- }
-
- $all_servers = Server::all();
- $servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
- foreach ($servers as $server) {
- PullHelperImageJob::dispatch($server);
- }
+ PullHelperImageJob::dispatch($this->server);
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
diff --git a/app/Actions/Server/ValidateServer.php b/app/Actions/Server/ValidateServer.php
index d0a4cd6be..55b37a77c 100644
--- a/app/Actions/Server/ValidateServer.php
+++ b/app/Actions/Server/ValidateServer.php
@@ -9,6 +9,8 @@ class ValidateServer
{
use AsAction;
+ public string $jobQueue = 'high';
+
public ?string $uptime = null;
public ?string $error = null;
diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php
index 9c4b0349c..9b87454da 100644
--- a/app/Actions/Service/DeleteService.php
+++ b/app/Actions/Service/DeleteService.php
@@ -4,6 +4,7 @@ namespace App\Actions\Service;
use App\Actions\Server\CleanupDocker;
use App\Models\Service;
+use Illuminate\Support\Facades\Log;
use Lorisleiva\Actions\Concerns\AsAction;
class DeleteService
@@ -39,7 +40,8 @@ class DeleteService
if (! empty($commands)) {
foreach ($commands as $command) {
$result = instant_remote_process([$command], $server, false);
- if ($result !== 0) {
+ if ($result !== null && $result !== 0) {
+ Log::error('Error deleting volumes: '.$result);
}
}
}
diff --git a/app/Actions/Service/RestartService.php b/app/Actions/Service/RestartService.php
index 1b6a5c32c..4151ea947 100644
--- a/app/Actions/Service/RestartService.php
+++ b/app/Actions/Service/RestartService.php
@@ -9,6 +9,8 @@ class RestartService
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Service $service)
{
StopService::run($service);
diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php
index 82de066d7..1dfaf6c49 100644
--- a/app/Actions/Service/StartService.php
+++ b/app/Actions/Service/StartService.php
@@ -10,6 +10,8 @@ class StartService
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Service $service)
{
$service->saveComposeConfigs();
diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php
index 046d94ced..95b08b437 100644
--- a/app/Actions/Service/StopService.php
+++ b/app/Actions/Service/StopService.php
@@ -10,6 +10,8 @@ class StopService
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{
try {
diff --git a/app/Console/Commands/CleanupRedis.php b/app/Console/Commands/CleanupRedis.php
index 5fc2b4e61..e16a82be4 100644
--- a/app/Console/Commands/CleanupRedis.php
+++ b/app/Console/Commands/CleanupRedis.php
@@ -13,7 +13,6 @@ class CleanupRedis extends Command
public function handle()
{
- echo "Cleanup Redis keys.\n";
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');
diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php
index 9d36ce9b8..def3d5a2c 100644
--- a/app/Console/Commands/CleanupStuckedResources.php
+++ b/app/Console/Commands/CleanupStuckedResources.php
@@ -30,7 +30,6 @@ class CleanupStuckedResources extends Command
public function handle()
{
- echo "Running cleanup stucked resources.\n";
$this->cleanup_stucked_resources();
}
diff --git a/app/Console/Commands/CloudCheckSubscription.php b/app/Console/Commands/CloudCheckSubscription.php
new file mode 100644
index 000000000..6e237e84b
--- /dev/null
+++ b/app/Console/Commands/CloudCheckSubscription.php
@@ -0,0 +1,49 @@
+get();
+ foreach ($activeSubscribers as $team) {
+ $stripeSubscriptionId = $team->subscription->stripe_subscription_id;
+ $stripeInvoicePaid = $team->subscription->stripe_invoice_paid;
+ $stripeCustomerId = $team->subscription->stripe_customer_id;
+ if (! $stripeSubscriptionId) {
+ echo "Team {$team->id} has no subscription, but invoice status is: {$stripeInvoicePaid}\n";
+ echo "Link on Stripe: https://dashboard.stripe.com/customers/{$stripeCustomerId}\n";
+
+ continue;
+ }
+ $subscription = $stripe->subscriptions->retrieve($stripeSubscriptionId);
+ if ($subscription->status === 'active') {
+ continue;
+ }
+ echo "Subscription {$stripeSubscriptionId} is not active ({$subscription->status})\n";
+ echo "Link on Stripe: https://dashboard.stripe.com/subscriptions/{$stripeSubscriptionId}\n";
+ }
+ }
+}
diff --git a/app/Console/Commands/CloudCleanupSubscriptions.php b/app/Console/Commands/CloudCleanupSubscriptions.php
index 8bb420ab8..9198b003e 100644
--- a/app/Console/Commands/CloudCleanupSubscriptions.php
+++ b/app/Console/Commands/CloudCleanupSubscriptions.php
@@ -36,7 +36,7 @@ class CloudCleanupSubscriptions extends Command
}
// If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status
if (! (data_get($team, 'subscription.stripe_subscription_id'))) {
- $this->info("Resetting invoice paid status for team {$team->id} {$team->name}");
+ $this->info("Resetting invoice paid status for team {$team->id}");
$team->subscription->update([
'stripe_invoice_paid' => false,
@@ -61,9 +61,9 @@ class CloudCleanupSubscriptions extends Command
$this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id'));
$confirm = $this->confirm('Do you want to cancel the subscription?', true);
if (! $confirm) {
- $this->info("Skipping team {$team->id} {$team->name}");
+ $this->info("Skipping team {$team->id}");
} else {
- $this->info("Cancelling subscription for team {$team->id} {$team->name}");
+ $this->info("Cancelling subscription for team {$team->id}");
$team->subscription->update([
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,
diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php
index f5f1233fe..962000d07 100644
--- a/app/Console/Commands/Dev.php
+++ b/app/Console/Commands/Dev.php
@@ -6,6 +6,7 @@ use App\Models\InstanceSettings;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Process;
+use Symfony\Component\Yaml\Yaml;
class Dev extends Command
{
@@ -31,19 +32,32 @@ class Dev extends Command
{
// Generate OpenAPI documentation
echo "Generating OpenAPI documentation.\n";
- $process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
+ // https://github.com/OAI/OpenAPI-Specification/releases
+ $process = Process::run([
+ '/var/www/html/vendor/bin/openapi',
+ 'app',
+ '-o',
+ 'openapi.yaml',
+ '--version',
+ '3.1.0',
+ ]);
$error = $process->errorOutput();
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
$error = preg_replace('/^\h*\v+/m', '', $error);
echo $error;
echo $process->output();
+ // Convert YAML to JSON
+ $yaml = file_get_contents('openapi.yaml');
+ $json = json_encode(Yaml::parse($yaml), JSON_PRETTY_PRINT);
+ file_put_contents('openapi.json', $json);
+ echo "Converted OpenAPI YAML to JSON.\n";
}
public function init()
{
// Generate APP_KEY if not exists
- if (empty(env('APP_KEY'))) {
+ if (empty(config('app.key'))) {
echo "Generating APP_KEY.\n";
Artisan::call('key:generate');
}
diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php
index cda4ca84f..f0e0e7fa0 100644
--- a/app/Console/Commands/Emails.php
+++ b/app/Console/Commands/Emails.php
@@ -187,7 +187,7 @@ class Emails extends Command
'team_id' => 0,
]);
}
- $this->mail = (new BackupSuccess($backup, $db))->toMail();
+ // $this->mail = (new BackupSuccess($backup->frequency, $db->name))->toMail();
$this->sendEmail();
break;
// case 'invitation-link':
diff --git a/app/Console/Commands/Horizon.php b/app/Console/Commands/Horizon.php
index 65a142d6e..655729ec9 100644
--- a/app/Console/Commands/Horizon.php
+++ b/app/Console/Commands/Horizon.php
@@ -12,8 +12,8 @@ class Horizon extends Command
public function handle()
{
- if (config('coolify.is_horizon_enabled')) {
- $this->info('Horizon is enabled. Starting.');
+ if (config('constants.horizon.is_horizon_enabled')) {
+ $this->info('[x]: Horizon is enabled. Starting.');
$this->call('horizon');
exit(0);
} else {
diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
index 8d4f51d1c..216262819 100644
--- a/app/Console/Commands/Init.php
+++ b/app/Console/Commands/Init.php
@@ -2,9 +2,9 @@
namespace App\Console\Commands;
-use App\Actions\Server\StopSentinel;
use App\Enums\ActivityTypes;
use App\Enums\ApplicationDeploymentStatus;
+use App\Jobs\CheckHelperImageJob;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
use App\Models\ScheduledDatabaseBackup;
@@ -12,6 +12,7 @@ use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\User;
use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
@@ -25,6 +26,8 @@ class Init extends Command
public function handle()
{
+ $this->optimize();
+
if (isCloud() && ! $this->option('force-cloud')) {
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
@@ -39,7 +42,6 @@ class Init extends Command
}
// Backward compatibility
- $this->disable_metrics();
$this->replace_slash_in_environment_name();
$this->restore_coolify_db_backup();
$this->update_user_emails();
@@ -53,16 +55,32 @@ class Init extends Command
} else {
$this->cleanup_in_progress_application_deployments();
}
+ echo "[3]: Cleanup Redis keys.\n";
$this->call('cleanup:redis');
+
+ echo "[4]: Cleanup stucked resources.\n";
$this->call('cleanup:stucked-resources');
+ try {
+ $this->pullHelperImage();
+ } catch (\Throwable $e) {
+ //
+ }
+
if (isCloud()) {
- $response = Http::retry(3, 1000)->get(config('constants.services.official'));
- if ($response->successful()) {
- $services = $response->json();
- File::put(base_path('templates/service-templates.json'), json_encode($services));
+ try {
+ $this->pullTemplatesFromCDN();
+ } catch (\Throwable $e) {
+ echo "Could not pull templates from CDN: {$e->getMessage()}\n";
+ }
+ }
+
+ if (! isCloud()) {
+ try {
+ $this->pullTemplatesFromCDN();
+ } catch (\Throwable $e) {
+ echo "Could not pull templates from CDN: {$e->getMessage()}\n";
}
- } else {
try {
$localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration();
@@ -70,8 +88,8 @@ class Init extends Command
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
$settings = instanceSettings();
- if (! is_null(env('AUTOUPDATE', null))) {
- if (env('AUTOUPDATE') == true) {
+ if (! is_null(config('constants.coolify.autoupdate', null))) {
+ if (config('constants.coolify.autoupdate') == true) {
$settings->update(['is_auto_update_enabled' => true]);
} else {
$settings->update(['is_auto_update_enabled' => false]);
@@ -80,20 +98,27 @@ class Init extends Command
}
}
- private function disable_metrics()
+ private function pullHelperImage()
{
- if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
- foreach ($this->servers as $server) {
- if ($server->settings->is_metrics_enabled === true) {
- $server->settings->update(['is_metrics_enabled' => false]);
- }
- if ($server->isFunctional()) {
- StopSentinel::dispatch($server);
- }
- }
+ CheckHelperImageJob::dispatch();
+ }
+
+ private function pullTemplatesFromCDN()
+ {
+ $response = Http::retry(3, 1000)->get(config('constants.services.official'));
+ if ($response->successful()) {
+ $services = $response->json();
+ File::put(base_path('templates/service-templates.json'), json_encode($services));
}
}
+ private function optimize()
+ {
+ echo "[1]: Optimizing Laravel (caching config, routes, views).\n";
+ Artisan::call('optimize:clear');
+ Artisan::call('optimize');
+ }
+
private function update_user_emails()
{
try {
@@ -175,7 +200,7 @@ class Init extends Command
private function restore_coolify_db_backup()
{
- if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
+ if (version_compare('4.0.0-beta.179', config('constants.coolify.version'), '<=')) {
try {
$database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) {
@@ -203,19 +228,19 @@ class Init extends Command
private function send_alive_signal()
{
$id = config('app.id');
- $version = config('version');
+ $version = config('constants.coolify.version');
$settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) {
- echo "Skipping alive as do_not_track is enabled\n";
+ echo "[2]: Skipping sending live signal as do_not_track is enabled\n";
return;
}
try {
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
- echo "I am alive!\n";
+ echo "[2]: Sending live signal!\n";
} catch (\Throwable $e) {
- echo "Error in alive: {$e->getMessage()}\n";
+ echo "[2]: Error in sending live signal: {$e->getMessage()}\n";
}
}
@@ -239,7 +264,7 @@ class Init extends Command
private function replace_slash_in_environment_name()
{
- if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
+ if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) {
$environments = Environment::all();
foreach ($environments as $environment) {
if (str_contains($environment->name, '/')) {
diff --git a/app/Console/Commands/OpenApi.php b/app/Console/Commands/OpenApi.php
index e248aa2c0..6cbcb310c 100644
--- a/app/Console/Commands/OpenApi.php
+++ b/app/Console/Commands/OpenApi.php
@@ -15,7 +15,15 @@ class OpenApi extends Command
{
// Generate OpenAPI documentation
echo "Generating OpenAPI documentation.\n";
- $process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
+ // https://github.com/OAI/OpenAPI-Specification/releases
+ $process = Process::run([
+ '/var/www/html/vendor/bin/openapi',
+ 'app',
+ '-o',
+ 'openapi.yaml',
+ '--version',
+ '3.1.0',
+ ]);
$error = $process->errorOutput();
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
$error = preg_replace('/^\h*\v+/m', '', $error);
diff --git a/app/Console/Commands/Scheduler.php b/app/Console/Commands/Scheduler.php
index 304cb357d..9ee7b06e6 100644
--- a/app/Console/Commands/Scheduler.php
+++ b/app/Console/Commands/Scheduler.php
@@ -12,8 +12,8 @@ class Scheduler extends Command
public function handle()
{
- if (config('coolify.is_scheduler_enabled')) {
- $this->info('Scheduler is enabled. Starting.');
+ if (config('constants.horizon.is_scheduler_enabled')) {
+ $this->info('[x]: Scheduler is enabled. Starting.');
$this->call('schedule:work');
exit(0);
} else {
diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php
index 1559e5f6d..b45707c5c 100644
--- a/app/Console/Commands/ServicesGenerate.php
+++ b/app/Console/Commands/ServicesGenerate.php
@@ -20,7 +20,10 @@ class ServicesGenerate extends Command
public function handle(): int
{
- $serviceTemplatesJson = collect(glob(base_path('templates/compose/*.yaml')))
+ $serviceTemplatesJson = collect(array_merge(
+ glob(base_path('templates/compose/*.yaml')),
+ glob(base_path('templates/compose/*.yml'))
+ ))
->mapWithKeys(function ($file): array {
$file = basename($file);
$parsed = $this->processFile($file);
@@ -68,7 +71,7 @@ class ServicesGenerate extends Command
'slogan' => $data->get('slogan', str($file)->headline()),
'compose' => $compose,
'tags' => $tags,
- 'logo' => $data->get('logo', 'svgs/coolify.png'),
+ 'logo' => $data->get('logo', 'svgs/default.webp'),
'minversion' => $data->get('minversion', '0.0.0'),
];
diff --git a/app/Console/Commands/SyncBunny.php b/app/Console/Commands/SyncBunny.php
index 228467f88..df1903828 100644
--- a/app/Console/Commands/SyncBunny.php
+++ b/app/Console/Commands/SyncBunny.php
@@ -57,7 +57,7 @@ class SyncBunny extends Command
PendingRequest::macro('storage', function ($fileName) use ($that) {
$headers = [
- 'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
+ 'AccessKey' => config('constants.bunny.storage_api_key'),
'Accept' => 'application/json',
'Content-Type' => 'application/octet-stream',
];
@@ -69,7 +69,7 @@ class SyncBunny extends Command
});
PendingRequest::macro('purge', function ($url) use ($that) {
$headers = [
- 'AccessKey' => env('BUNNY_API_KEY'),
+ 'AccessKey' => config('constants.bunny.api_key'),
'Accept' => 'application/json',
];
$that->info('Purging: '.$url);
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 8b60c694b..19d22ae21 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -28,85 +28,100 @@ class Kernel extends ConsoleKernel
{
private $allServers;
+ private Schedule $scheduleInstance;
+
private InstanceSettings $settings;
+ private string $updateCheckFrequency;
+
+ private string $instanceTimezone;
+
protected function schedule(Schedule $schedule): void
{
+ $this->scheduleInstance = $schedule;
$this->allServers = Server::where('ip', '!=', '1.2.3.4');
$this->settings = instanceSettings();
+ $this->updateCheckFrequency = $this->settings->update_check_frequency ?: '0 * * * *';
- $schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
+ $this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone');
+
+ if (validate_timezone($this->instanceTimezone) === false) {
+ $this->instanceTimezone = config('app.timezone');
+ }
+
+ // $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
if (isDev()) {
// Instance Jobs
- $schedule->command('horizon:snapshot')->everyMinute();
- $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
- $schedule->job(new CheckHelperImageJob)->everyFiveMinutes()->onOneServer();
+ $this->scheduleInstance->command('horizon:snapshot')->everyMinute();
+ $this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
+ $this->scheduleInstance->job(new CheckHelperImageJob)->everyTenMinutes()->onOneServer();
// Server Jobs
- $this->checkResources($schedule);
+ $this->checkResources();
- $this->checkScheduledBackups($schedule);
- $this->checkScheduledTasks($schedule);
+ $this->checkScheduledBackups();
+ $this->checkScheduledTasks();
- $schedule->command('uploads:clear')->everyTwoMinutes();
+ $this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
} else {
// Instance Jobs
- $schedule->command('horizon:snapshot')->everyFiveMinutes();
- $schedule->command('cleanup:unreachable-servers')->daily()->onOneServer();
- $schedule->job(new PullTemplatesFromCDN)->cron($this->settings->update_check_frequency)->timezone($this->settings->instance_timezone)->onOneServer();
- $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
- $this->scheduleUpdates($schedule);
+ $this->scheduleInstance->command('horizon:snapshot')->everyFiveMinutes();
+ $this->scheduleInstance->command('cleanup:unreachable-servers')->daily()->onOneServer();
+
+ $this->scheduleInstance->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
+
+ $this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
+ $this->scheduleUpdates();
// Server Jobs
- $this->checkResources($schedule);
+ $this->checkResources();
- $this->pullImages($schedule);
+ $this->pullImages();
- $this->checkScheduledBackups($schedule);
- $this->checkScheduledTasks($schedule);
+ $this->checkScheduledBackups();
+ $this->checkScheduledTasks();
- $schedule->command('cleanup:database --yes')->daily();
- $schedule->command('uploads:clear')->everyTwoMinutes();
+ $this->scheduleInstance->command('cleanup:database --yes')->daily();
+ $this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
}
}
- private function pullImages($schedule): void
+ private function pullImages(): void
{
$servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
foreach ($servers as $server) {
if ($server->isSentinelEnabled()) {
- $schedule->job(function () use ($server) {
+ $this->scheduleInstance->job(function () use ($server) {
CheckAndStartSentinelJob::dispatch($server);
- })->cron($this->settings->update_check_frequency)->timezone($this->settings->instance_timezone)->onOneServer();
+ })->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
}
}
- $schedule->job(new CheckHelperImageJob)
- ->cron($this->settings->update_check_frequency)
- ->timezone($this->settings->instance_timezone)
+ $this->scheduleInstance->job(new CheckHelperImageJob)
+ ->cron($this->updateCheckFrequency)
+ ->timezone($this->instanceTimezone)
->onOneServer();
}
- private function scheduleUpdates($schedule): void
+ private function scheduleUpdates(): void
{
- $updateCheckFrequency = $this->settings->update_check_frequency;
- $schedule->job(new CheckForUpdatesJob)
- ->cron($updateCheckFrequency)
- ->timezone($this->settings->instance_timezone)
+ $this->scheduleInstance->job(new CheckForUpdatesJob)
+ ->cron($this->updateCheckFrequency)
+ ->timezone($this->instanceTimezone)
->onOneServer();
if ($this->settings->is_auto_update_enabled) {
$autoUpdateFrequency = $this->settings->auto_update_frequency;
- $schedule->job(new UpdateCoolifyJob)
+ $this->scheduleInstance->job(new UpdateCoolifyJob)
->cron($autoUpdateFrequency)
- ->timezone($this->settings->instance_timezone)
+ ->timezone($this->instanceTimezone)
->onOneServer();
}
}
- private function checkResources($schedule): void
+ private function checkResources(): void
{
if (isCloud()) {
$servers = $this->allServers->whereHas('team.subscription')->get();
@@ -115,40 +130,46 @@ class Kernel extends ConsoleKernel
} else {
$servers = $this->allServers->get();
}
- // $schedule->job(new \App\Jobs\ResourcesCheck)->everyMinute()->onOneServer();
foreach ($servers as $server) {
- $serverTimezone = $server->settings->server_timezone;
+ $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
// Sentinel check
$lastSentinelUpdate = $server->sentinel_updated_at;
if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) {
// Check container status every minute if Sentinel does not activated
- $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
- // $schedule->job(new \App\Jobs\ServerCheckNewJob($server))->everyMinute()->onOneServer();
+ if (validate_timezone($serverTimezone) === false) {
+ $serverTimezone = config('app.timezone');
+ }
+ if (isCloud()) {
+ $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer();
+ } else {
+ $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer();
+ }
+ // $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer();
// Check storage usage every 10 minutes if Sentinel does not activated
- $schedule->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer();
+ $this->scheduleInstance->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer();
}
if ($server->settings->force_docker_cleanup) {
- $schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
+ $this->scheduleInstance->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
} else {
- $schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
+ $this->scheduleInstance->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
}
// Cleanup multiplexed connections every hour
- $schedule->job(new ServerCleanupMux($server))->hourly()->onOneServer();
+ // $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
// Temporary solution until we have better memory management for Sentinel
if ($server->isSentinelEnabled()) {
- $schedule->job(function () use ($server) {
+ $this->scheduleInstance->job(function () use ($server) {
$server->restartContainer('coolify-sentinel');
})->daily()->onOneServer();
}
}
}
- private function checkScheduledBackups($schedule): void
+ private function checkScheduledBackups(): void
{
$scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get();
if ($scheduled_backups->isEmpty()) {
@@ -166,27 +187,23 @@ class Kernel extends ConsoleKernel
if (is_null($server)) {
continue;
}
- $serverTimezone = $server->settings->server_timezone;
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
}
- $schedule->job(new DatabaseBackupJob(
+ $this->scheduleInstance->job(new DatabaseBackupJob(
backup: $scheduled_backup
- ))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
+ ))->cron($scheduled_backup->frequency)->timezone($this->instanceTimezone)->onOneServer();
}
}
- private function checkScheduledTasks($schedule): void
+ private function checkScheduledTasks(): void
{
- $scheduled_tasks = ScheduledTask::all();
+ $scheduled_tasks = ScheduledTask::where('enabled', true)->get();
if ($scheduled_tasks->isEmpty()) {
return;
}
foreach ($scheduled_tasks as $scheduled_task) {
- if ($scheduled_task->enabled === false) {
- continue;
- }
$service = $scheduled_task->service;
$application = $scheduled_task->application;
@@ -210,14 +227,13 @@ class Kernel extends ConsoleKernel
if (! $server) {
continue;
}
- $serverTimezone = $server->settings->server_timezone ?: config('app.timezone');
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
}
- $schedule->job(new ScheduledTaskJob(
+ $this->scheduleInstance->job(new ScheduledTaskJob(
task: $scheduled_task
- ))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
+ ))->cron($scheduled_task->frequency)->timezone($this->instanceTimezone)->onOneServer();
}
}
diff --git a/app/Events/DatabaseProxyStopped.php b/app/Events/DatabaseProxyStopped.php
new file mode 100644
index 000000000..96b35a5ca
--- /dev/null
+++ b/app/Events/DatabaseProxyStopped.php
@@ -0,0 +1,35 @@
+currentTeam()?->id ?? null;
+ }
+ if (is_null($teamId)) {
+ throw new \Exception('Team id is null');
+ }
+ $this->teamId = $teamId;
+ }
+
+ public function broadcastOn(): array
+ {
+ return [
+ new PrivateChannel("team.{$this->teamId}"),
+ ];
+ }
+}
diff --git a/app/Events/DatabaseStatusChanged.php b/app/Events/DatabaseStatusChanged.php
index a94bc2272..913b21bc2 100644
--- a/app/Events/DatabaseStatusChanged.php
+++ b/app/Events/DatabaseStatusChanged.php
@@ -7,27 +7,29 @@ use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Auth;
class DatabaseStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
- public ?string $userId = null;
+ public $userId = null;
public function __construct($userId = null)
{
if (is_null($userId)) {
- $userId = auth()->user()->id ?? null;
+ $userId = Auth::id() ?? null;
}
if (is_null($userId)) {
return false;
}
+
$this->userId = $userId;
}
public function broadcastOn(): ?array
{
- if ($this->userId) {
+ if (! is_null($this->userId)) {
return [
new PrivateChannel("user.{$this->userId}"),
];
diff --git a/app/Events/ScheduledTaskDone.php b/app/Events/ScheduledTaskDone.php
new file mode 100644
index 000000000..c8b5547f6
--- /dev/null
+++ b/app/Events/ScheduledTaskDone.php
@@ -0,0 +1,34 @@
+user()->currentTeam()->id ?? null;
+ }
+ if (is_null($teamId)) {
+ throw new \Exception('Team id is null');
+ }
+ $this->teamId = $teamId;
+ }
+
+ public function broadcastOn(): array
+ {
+ return [
+ new PrivateChannel("team.{$this->teamId}"),
+ ];
+ }
+}
diff --git a/app/Events/ServiceStatusChanged.php b/app/Events/ServiceStatusChanged.php
index a86a8b02d..3950022e1 100644
--- a/app/Events/ServiceStatusChanged.php
+++ b/app/Events/ServiceStatusChanged.php
@@ -7,6 +7,7 @@ use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Auth;
class ServiceStatusChanged implements ShouldBroadcast
{
@@ -17,7 +18,7 @@ class ServiceStatusChanged implements ShouldBroadcast
public function __construct($userId = null)
{
if (is_null($userId)) {
- $userId = auth()->user()->id ?? null;
+ $userId = Auth::id() ?? null;
}
if (is_null($userId)) {
return false;
diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php
index 1a2146799..8da476b9e 100644
--- a/app/Helpers/SshMultiplexingHelper.php
+++ b/app/Helpers/SshMultiplexingHelper.php
@@ -21,17 +21,14 @@ class SshMultiplexingHelper
];
}
- public static function ensureMultiplexedConnection(Server $server)
+ public static function ensureMultiplexedConnection(Server $server): bool
{
if (! self::isMultiplexingEnabled()) {
- return;
+ return false;
}
$sshConfig = self::serverSshConfiguration($server);
$muxSocket = $sshConfig['muxFilename'];
- $sshKeyLocation = $sshConfig['sshKeyLocation'];
-
- self::validateSshKey($sshKeyLocation);
$checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
@@ -41,16 +38,17 @@ class SshMultiplexingHelper
$process = Process::run($checkCommand);
if ($process->exitCode() !== 0) {
- self::establishNewMultiplexedConnection($server);
+ return self::establishNewMultiplexedConnection($server);
}
+
+ return true;
}
- public static function establishNewMultiplexedConnection(Server $server)
+ public static function establishNewMultiplexedConnection(Server $server): bool
{
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
-
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time');
@@ -60,15 +58,14 @@ class SshMultiplexingHelper
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
-
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
$establishCommand .= "{$server->user}@{$server->ip}";
-
$establishProcess = Process::run($establishCommand);
-
if ($establishProcess->exitCode() !== 0) {
- throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
+ return false;
}
+
+ return true;
}
public static function removeMuxFile(Server $server)
@@ -97,9 +94,8 @@ class SshMultiplexingHelper
if ($server->isIpv6()) {
$scp_command .= '-6 ';
}
- if (self::isMultiplexingEnabled()) {
+ if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) {
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
- self::ensureMultiplexedConnection($server);
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
@@ -120,6 +116,9 @@ class SshMultiplexingHelper
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
+
+ self::validateSshKey($server->privateKey);
+
$muxSocket = $sshConfig['muxFilename'];
$timeout = config('constants.ssh.command_timeout');
@@ -127,9 +126,8 @@ class SshMultiplexingHelper
$ssh_command = "timeout $timeout ssh ";
- if (self::isMultiplexingEnabled()) {
+ if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) {
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
- self::ensureMultiplexedConnection($server);
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
@@ -151,16 +149,17 @@ class SshMultiplexingHelper
private static function isMultiplexingEnabled(): bool
{
- return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop');
+ return config('constants.ssh.mux_enabled') && ! config('constants.coolify.is_windows_docker_desktop');
}
- private static function validateSshKey(string $sshKeyLocation): void
+ private static function validateSshKey(PrivateKey $privateKey): void
{
- $checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
+ $keyLocation = $privateKey->getKeyLocation();
+ $checkKeyCommand = "ls $keyLocation 2>/dev/null";
$keyCheckProcess = Process::run($checkKeyCommand);
if ($keyCheckProcess->exitCode() !== 0) {
- throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
+ $privateKey->storeInFileSystem();
}
}
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index 6402ec651..52d81499b 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -70,7 +70,8 @@ class ApplicationsController extends Controller
items: new OA\Items(ref: '#/components/schemas/Application')
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -180,8 +181,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
- )),
- ]),
+ )
+ ),
+ ]
+ ),
responses: [
new OA\Response(
response: 200,
@@ -284,8 +287,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
- )),
- ]),
+ )
+ ),
+ ]
+ ),
responses: [
new OA\Response(
response: 200,
@@ -388,8 +393,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
- )),
- ]),
+ )
+ ),
+ ]
+ ),
responses: [
new OA\Response(
response: 200,
@@ -476,8 +483,10 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
- )),
- ]),
+ )
+ ),
+ ]
+ ),
responses: [
new OA\Response(
response: 200,
@@ -561,8 +570,10 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
- )),
- ]),
+ )
+ ),
+ ]
+ ),
responses: [
new OA\Response(
response: 200,
@@ -612,8 +623,10 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
- )),
- ]),
+ )
+ ),
+ ]
+ ),
responses: [
new OA\Response(
response: 200,
@@ -636,7 +649,7 @@ class ApplicationsController extends Controller
private function create_application(Request $request, $type)
{
- $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image'];
+ $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -676,6 +689,27 @@ class ApplicationsController extends Controller
$githubAppUuid = $request->github_app_uuid;
$useBuildServer = $request->use_build_server;
$isStatic = $request->is_static;
+ $customNginxConfiguration = $request->custom_nginx_configuration;
+
+ if (! is_null($customNginxConfiguration)) {
+ if (! isBase64Encoded($customNginxConfiguration)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $customNginxConfiguration = base64_decode($customNginxConfiguration);
+ if (mb_detect_encoding($customNginxConfiguration, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ }
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
if (! $project) {
@@ -1247,7 +1281,8 @@ class ApplicationsController extends Controller
ref: '#/components/schemas/Application'
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1319,7 +1354,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1445,8 +1481,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
- )),
- ]),
+ )
+ ),
+ ]
+ ),
responses: [
new OA\Response(
response: 200,
@@ -1461,7 +1499,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1500,7 +1539,7 @@ class ApplicationsController extends Controller
], 404);
}
$server = $application->destination->server;
- $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server'];
+ $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration'];
$validationRules = [
'name' => 'string|max:255',
@@ -1512,6 +1551,7 @@ class ApplicationsController extends Controller
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
+ 'custom_nginx_configuration' => 'string|nullable',
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
@@ -1530,6 +1570,25 @@ class ApplicationsController extends Controller
}
}
}
+ if ($request->has('custom_nginx_configuration')) {
+ if (! isBase64Encoded($request->custom_nginx_configuration)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $customNginxConfiguration = base64_decode($request->custom_nginx_configuration);
+ if (mb_detect_encoding($customNginxConfiguration, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ }
$return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
@@ -1550,16 +1609,33 @@ class ApplicationsController extends Controller
}
$domains = $request->domains;
if ($request->has('domains') && $server->isProxyShouldRun()) {
- $errors = [];
+ $uuid = $request->uuid;
$fqdn = $request->domains;
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
- $application->fqdn = $fqdn;
- if (! $application->settings->is_container_label_readonly_enabled) {
- $customLabels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
- $application->custom_labels = base64_encode($customLabels);
+ $errors = [];
+ $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
+ $domain = trim($domain);
+ if (filter_var($domain, FILTER_VALIDATE_URL) === false || ! preg_match('/^https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}/', $domain)) {
+ $errors[] = 'Invalid domain: '.$domain;
+ }
+
+ return $domain;
+ });
+ if (count($errors) > 0) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $errors,
+ ], 422);
+ }
+ if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'domains' => 'One of the domain is already used.',
+ ],
+ ], 422);
}
- $request->offsetUnset('domains');
}
$dockerComposeDomainsJson = collect();
@@ -1649,7 +1725,8 @@ class ApplicationsController extends Controller
items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1755,7 +1832,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1943,7 +2021,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2124,7 +2203,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2273,7 +2353,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2365,9 +2446,11 @@ class ApplicationsController extends Controller
properties: [
'message' => ['type' => 'string', 'example' => 'Deployment request queued.', 'description' => 'Message.'],
'deployment_uuid' => ['type' => 'string', 'example' => 'doogksw', 'description' => 'UUID of the deployment.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2453,7 +2536,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2527,7 +2611,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index 821a40b79..f0c052cdc 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -211,8 +211,9 @@ class DatabasesController extends Controller
'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'],
'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'],
'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'],
- 'mongo_initdb_init_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
+ 'mongo_initdb_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
+ 'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@@ -241,7 +242,7 @@ class DatabasesController extends Controller
)]
public function update_by_uuid(Request $request)
{
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -413,12 +414,12 @@ class DatabasesController extends Controller
}
break;
case 'standalone-mongodb':
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string',
- 'mongo_initdb_init_database' => 'string',
+ 'mongo_initdb_database' => 'string',
]);
if ($request->has('mongo_conf')) {
if (! isBase64Encoded($request->mongo_conf)) {
@@ -443,9 +444,10 @@ class DatabasesController extends Controller
break;
case 'standalone-mysql':
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
+ 'mysql_password' => 'string',
'mysql_user' => 'string',
'mysql_database' => 'string',
'mysql_conf' => 'string',
@@ -909,6 +911,7 @@ class DatabasesController extends Controller
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
+ 'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@@ -1013,7 +1016,7 @@ class DatabasesController extends Controller
public function create_database(Request $request, NewDatabaseTypes $type)
{
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -1220,9 +1223,10 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MYSQL) {
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
+ 'mysql_password' => 'string',
'mysql_user' => 'string',
'mysql_database' => 'string',
'mysql_conf' => 'string',
@@ -1456,12 +1460,12 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MONGODB) {
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string',
- 'mongo_initdb_init_database' => 'string',
+ 'mongo_initdb_database' => 'string',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
@@ -1557,7 +1561,8 @@ class DatabasesController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1632,9 +1637,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database starting request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1708,9 +1715,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database stopping request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1784,9 +1793,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database restaring request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php
index 062cc04e7..303e6535d 100644
--- a/app/Http/Controllers/Api/OtherController.php
+++ b/app/Http/Controllers/Api/OtherController.php
@@ -37,7 +37,7 @@ class OtherController extends Controller
)]
public function version(Request $request)
{
- return response(config('version'));
+ return response(config('constants.coolify.version'));
}
#[OA\Get(
@@ -147,7 +147,7 @@ class OtherController extends Controller
public function feedback(Request $request)
{
$content = $request->input('content');
- $webhook_url = config('coolify.feedback_discord_webhook');
+ $webhook_url = config('constants.webhooks.feedback_discord_webhook');
if ($webhook_url) {
Http::post($webhook_url, [
'content' => $content,
diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php
index 491179d5d..1d89c82ed 100644
--- a/app/Http/Controllers/Api/ProjectController.php
+++ b/app/Http/Controllers/Api/ProjectController.php
@@ -116,7 +116,7 @@ class ProjectController extends Controller
responses: [
new OA\Response(
response: 200,
- description: 'Project details',
+ description: 'Environment details',
content: new OA\JsonContent(ref: '#/components/schemas/Environment')),
new OA\Response(
response: 401,
@@ -422,7 +422,7 @@ class ProjectController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
- if ($project->resource_count() > 0) {
+ if (! $project->isEmpty()) {
return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400);
}
diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php
index aa636983f..cfa4d0d6c 100644
--- a/app/Http/Controllers/Api/SecurityController.php
+++ b/app/Http/Controllers/Api/SecurityController.php
@@ -81,15 +81,8 @@ class SecurityController extends Controller
new OA\Response(
response: 200,
description: 'Get all private keys.',
- content: [
- new OA\MediaType(
- mediaType: 'application/json',
- schema: new OA\Schema(
- type: 'array',
- items: new OA\Items(ref: '#/components/schemas/PrivateKey')
- )
- ),
- ]),
+ content: new OA\JsonContent(ref: '#/components/schemas/PrivateKey')
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index 34498bbb6..7c512497d 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -426,6 +426,7 @@ class ServersController extends Controller
'private_key_uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the private key.'],
'is_build_server' => ['type' => 'boolean', 'example' => false, 'description' => 'Is build server.'],
'instant_validate' => ['type' => 'boolean', 'example' => false, 'description' => 'Instant validate.'],
+ 'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'example' => 'traefik', 'description' => 'The proxy type.'],
],
),
),
@@ -461,7 +462,7 @@ class ServersController extends Controller
)]
public function create_server(Request $request)
{
- $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
+ $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -481,6 +482,7 @@ class ServersController extends Controller
'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable',
+ 'proxy_type' => 'string|nullable',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
@@ -512,6 +514,14 @@ class ServersController extends Controller
if (is_null($request->instant_validate)) {
$request->offsetSet('instant_validate', false);
}
+ if ($request->proxy_type) {
+ $validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
+ return str($proxyType->value)->lower();
+ });
+ if (! $validProxyTypes->contains(str($request->proxy_type)->lower())) {
+ return response()->json(['message' => 'Invalid proxy type.'], 422);
+ }
+ }
$privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();
if (! $privateKey) {
return response()->json(['message' => 'Private key not found.'], 404);
@@ -521,6 +531,8 @@ class ServersController extends Controller
return response()->json(['message' => 'Server with this IP already exists.'], 400);
}
+ $proxyType = $request->proxy_type ? str($request->proxy_type)->upper() : ProxyTypes::TRAEFIK->value;
+
$server = ModelsServer::create([
'name' => $request->name,
'description' => $request->description,
@@ -530,7 +542,7 @@ class ServersController extends Controller
'private_key_id' => $privateKey->id,
'team_id' => $teamId,
'proxy' => [
- 'type' => ProxyTypes::TRAEFIK->value,
+ 'type' => $proxyType,
'status' => ProxyStatus::EXITED->value,
],
]);
@@ -555,6 +567,9 @@ class ServersController extends Controller
['bearerAuth' => []],
],
tags: ['Servers'],
+ parameters: [
+ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')),
+ ],
requestBody: new OA\RequestBody(
required: true,
description: 'Server updated.',
@@ -571,6 +586,7 @@ class ServersController extends Controller
'private_key_uuid' => ['type' => 'string', 'description' => 'The UUID of the private key.'],
'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'],
'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'],
+ 'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'description' => 'The proxy type.'],
],
),
),
@@ -583,8 +599,7 @@ class ServersController extends Controller
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
- type: 'array',
- items: new OA\Items(ref: '#/components/schemas/Server')
+ ref: '#/components/schemas/Server'
)
),
]),
@@ -604,7 +619,7 @@ class ServersController extends Controller
)]
public function update_server(Request $request)
{
- $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
+ $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -624,6 +639,7 @@ class ServersController extends Controller
'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable',
+ 'proxy_type' => 'string|nullable',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
@@ -644,6 +660,16 @@ class ServersController extends Controller
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
+ if ($request->proxy_type) {
+ $validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
+ return str($proxyType->value)->lower();
+ });
+ if ($validProxyTypes->contains(str($request->proxy_type)->lower())) {
+ $server->changeProxy($request->proxy_type, async: true);
+ } else {
+ return response()->json(['message' => 'Invalid proxy type.'], 422);
+ }
+ }
$server->update($request->only(['name', 'description', 'ip', 'port', 'user']));
if ($request->is_build_server) {
$server->settings()->update([
@@ -654,7 +680,9 @@ class ServersController extends Controller
ValidateServer::dispatch($server);
}
- return response()->json(serializeApiResponse($server))->setStatusCode(201);
+ return response()->json([
+ 'uuid' => $server->uuid,
+ ])->setStatusCode(201);
}
#[OA\Delete(
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index 581118e16..9f1e4eeb8 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -110,13 +110,19 @@ class Controller extends BaseController
return redirect()->route('login')->with('error', 'Invalid credentials.');
}
- public function accept_invitation()
+ public function acceptInvitation()
{
$resetPassword = request()->query('reset-password');
$invitationUuid = request()->route('uuid');
+
$invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail();
$user = User::whereEmail($invitation->email)->firstOrFail();
+
+ if (Auth::id() !== $user->id) {
+ abort(400, 'You are not allowed to accept this invitation.');
+ }
$invitationValid = $invitation->isValid();
+
if ($invitationValid) {
if ($resetPassword) {
$user->update([
@@ -131,14 +137,12 @@ class Controller extends BaseController
}
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
$invitation->delete();
- if (auth()->user()?->id !== $user->id) {
- return redirect()->route('login');
- }
+
refreshSession($invitation->team);
return redirect()->route('team.index');
} else {
- abort(401);
+ abort(400, 'Invitation expired.');
}
}
@@ -146,10 +150,10 @@ class Controller extends BaseController
{
$invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail();
$user = User::whereEmail($invitation->email)->firstOrFail();
- if (is_null(auth()->user())) {
+ if (is_null(Auth::user())) {
return redirect()->route('login');
}
- if (auth()->user()->id !== $user->id) {
+ if (Auth::id() !== $user->id) {
abort(401);
}
$invitation->delete();
diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php
index 3683adaa8..ac1d4ded2 100644
--- a/app/Http/Controllers/Webhook/Github.php
+++ b/app/Http/Controllers/Webhook/Github.php
@@ -463,7 +463,7 @@ class Github extends Controller
$private_key = data_get($data, 'pem');
$webhook_secret = data_get($data, 'webhook_secret');
$private_key = PrivateKey::create([
- 'name' => $slug,
+ 'name' => "github-app-{$slug}",
'private_key' => $private_key,
'team_id' => $github_app->team_id,
'is_git_related' => true,
diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php
index f56711bad..d8dcc0c3b 100644
--- a/app/Http/Controllers/Webhook/Gitlab.php
+++ b/app/Http/Controllers/Webhook/Gitlab.php
@@ -33,6 +33,7 @@ class Gitlab extends Controller
return;
}
+
$return_payloads = collect([]);
$payload = $request->collect();
$headers = $request->headers->all();
@@ -48,6 +49,15 @@ class Gitlab extends Controller
return response($return_payloads);
}
+ if (empty($x_gitlab_token)) {
+ $return_payloads->push([
+ 'status' => 'failed',
+ 'message' => 'Invalid signature.',
+ ]);
+
+ return response($return_payloads);
+ }
+
if ($x_gitlab_event === 'push') {
$branch = data_get($payload, 'ref');
$full_name = data_get($payload, 'project.path_with_namespace');
diff --git a/app/Http/Controllers/Webhook/Stripe.php b/app/Http/Controllers/Webhook/Stripe.php
index 5d297b242..83ba16699 100644
--- a/app/Http/Controllers/Webhook/Stripe.php
+++ b/app/Http/Controllers/Webhook/Stripe.php
@@ -3,23 +3,26 @@
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
-use App\Jobs\ServerLimitCheckJob;
-use App\Jobs\SubscriptionInvoiceFailedJob;
-use App\Jobs\SubscriptionTrialEndedJob;
-use App\Jobs\SubscriptionTrialEndsSoonJob;
-use App\Models\Subscription;
-use App\Models\Team;
+use App\Jobs\StripeProcessJob;
use App\Models\Webhook;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
-use Illuminate\Support\Str;
class Stripe extends Controller
{
+ protected $webhook;
+
public function events(Request $request)
{
try {
+ $webhookSecret = config('subscription.stripe_webhook_secret');
+ $signature = $request->header('Stripe-Signature');
+ $event = \Stripe\Webhook::constructEvent(
+ $request->getContent(),
+ $signature,
+ $webhookSecret
+ );
if (app()->isDownForMaintenance()) {
$epoch = now()->valueOf();
$data = [
@@ -35,276 +38,17 @@ class Stripe extends Controller
$json = json_encode($data);
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json);
- return;
+ return response('Webhook received. Cool cool cool cool cool.', 200);
}
- $webhookSecret = config('subscription.stripe_webhook_secret');
- $signature = $request->header('Stripe-Signature');
- $excludedPlans = config('subscription.stripe_excluded_plans');
- $event = \Stripe\Webhook::constructEvent(
- $request->getContent(),
- $signature,
- $webhookSecret
- );
- $webhook = Webhook::create([
+ $this->webhook = Webhook::create([
'type' => 'stripe',
'payload' => $request->getContent(),
]);
- $type = data_get($event, 'type');
- $data = data_get($event, 'data.object');
- switch ($type) {
- case 'radar.early_fraud_warning.created':
- $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
- $id = data_get($data, 'id');
- $charge = data_get($data, 'charge');
- if ($charge) {
- $stripe->refunds->create(['charge' => $charge]);
- }
- $pi = data_get($data, 'payment_intent');
- $piData = $stripe->paymentIntents->retrieve($pi, []);
- $customerId = data_get($piData, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if ($subscription) {
- $subscriptionId = data_get($subscription, 'stripe_subscription_id');
- $stripe->subscriptions->cancel($subscriptionId, []);
- $subscription->update([
- 'stripe_invoice_paid' => false,
- ]);
- send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
- } else {
- send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
+ StripeProcessJob::dispatch($event);
- return response("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}", 400);
- }
- break;
- case 'checkout.session.completed':
- $clientReferenceId = data_get($data, 'client_reference_id');
- if (is_null($clientReferenceId)) {
- send_internal_notification('Checkout session completed without client reference id.');
- break;
- }
- $userId = Str::before($clientReferenceId, ':');
- $teamId = Str::after($clientReferenceId, ':');
- $subscriptionId = data_get($data, 'subscription');
- $customerId = data_get($data, 'customer');
- $team = Team::find($teamId);
- $found = $team->members->where('id', $userId)->first();
- if (! $found->isAdmin()) {
- send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
-
- return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.", 400);
- }
- $subscription = Subscription::where('team_id', $teamId)->first();
- if ($subscription) {
- // send_internal_notification('Old subscription activated for team: '.$teamId);
- $subscription->update([
- 'stripe_subscription_id' => $subscriptionId,
- 'stripe_customer_id' => $customerId,
- 'stripe_invoice_paid' => true,
- ]);
- } else {
- // send_internal_notification('New subscription for team: '.$teamId);
- Subscription::create([
- 'team_id' => $teamId,
- 'stripe_subscription_id' => $subscriptionId,
- 'stripe_customer_id' => $customerId,
- 'stripe_invoice_paid' => true,
- ]);
- }
- break;
- case 'invoice.paid':
- $customerId = data_get($data, 'customer');
- $planId = data_get($data, 'lines.data.0.plan.id');
- if (Str::contains($excludedPlans, $planId)) {
- // send_internal_notification('Subscription excluded.');
- break;
- }
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if ($subscription) {
- $subscription->update([
- 'stripe_invoice_paid' => true,
- ]);
- } else {
- return response("No subscription found for customer: {$customerId}", 400);
- }
- break;
- case 'invoice.payment_failed':
- $customerId = data_get($data, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if (! $subscription) {
- // send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
-
- return response('No subscription found in Coolify.');
- }
- $team = data_get($subscription, 'team');
- if (! $team) {
- // send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
-
- return response('No team found in Coolify.');
- }
- if (! $subscription->stripe_invoice_paid) {
- SubscriptionInvoiceFailedJob::dispatch($team);
- // send_internal_notification('Invoice payment failed: '.$customerId);
- } else {
- // send_internal_notification('Invoice payment failed but already paid: '.$customerId);
- }
- break;
- case 'payment_intent.payment_failed':
- $customerId = data_get($data, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if (! $subscription) {
- // send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
-
- return response('No subscription found in Coolify.');
- }
- if ($subscription->stripe_invoice_paid) {
- // send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
-
- return;
- }
- send_internal_notification('Subscription payment failed for customer: '.$customerId);
- break;
- case 'customer.subscription.created':
- $customerId = data_get($data, 'customer');
- $subscriptionId = data_get($data, 'id');
- $teamId = data_get($data, 'metadata.team_id');
- $userId = data_get($data, 'metadata.user_id');
- if (! $teamId || ! $userId) {
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if ($subscription) {
- return response("Subscription already exists for customer: {$customerId}", 200);
- }
-
- return response('No team id or user id found', 400);
- }
- $team = Team::find($teamId);
- $found = $team->members->where('id', $userId)->first();
- if (! $found->isAdmin()) {
- send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
-
- return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.", 400);
- }
- $subscription = Subscription::where('team_id', $teamId)->first();
- if ($subscription) {
- return response("Subscription already exists for team: {$teamId}", 200);
- } else {
- Subscription::create([
- 'team_id' => $teamId,
- 'stripe_subscription_id' => $subscriptionId,
- 'stripe_customer_id' => $customerId,
- 'stripe_invoice_paid' => false,
- ]);
-
- return response('Subscription created');
- }
- case 'customer.subscription.updated':
- $teamId = data_get($data, 'metadata.team_id');
- $userId = data_get($data, 'metadata.user_id');
- $customerId = data_get($data, 'customer');
- $status = data_get($data, 'status');
- $subscriptionId = data_get($data, 'items.data.0.subscription');
- $planId = data_get($data, 'items.data.0.plan.id');
- if (Str::contains($excludedPlans, $planId)) {
- // send_internal_notification('Subscription excluded.');
- break;
- }
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if (! $subscription) {
- if ($status === 'incomplete_expired') {
- return response('Subscription incomplete expired', 200);
- }
- if ($teamId) {
- $subscription = Subscription::create([
- 'team_id' => $teamId,
- 'stripe_subscription_id' => $subscriptionId,
- 'stripe_customer_id' => $customerId,
- 'stripe_invoice_paid' => false,
- ]);
- } else {
- return response('No subscription and team id found', 400);
- }
- }
- $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
- $feedback = data_get($data, 'cancellation_details.feedback');
- $comment = data_get($data, 'cancellation_details.comment');
- $lookup_key = data_get($data, 'items.data.0.price.lookup_key');
- if (str($lookup_key)->contains('dynamic')) {
- $quantity = data_get($data, 'items.data.0.quantity', 2);
- $team = data_get($subscription, 'team');
- if ($team) {
- $team->update([
- 'custom_server_limit' => $quantity,
- ]);
- }
- ServerLimitCheckJob::dispatch($team);
- }
- $subscription->update([
- 'stripe_feedback' => $feedback,
- 'stripe_comment' => $comment,
- 'stripe_plan_id' => $planId,
- 'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
- ]);
- if ($status === 'paused' || $status === 'incomplete_expired') {
- $subscription->update([
- 'stripe_invoice_paid' => false,
- ]);
- }
- if ($feedback) {
- $reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
- if ($comment) {
- $reason .= ' with comment: \''.$comment."'";
- }
- }
- break;
- case 'customer.subscription.deleted':
- // End subscription
- $customerId = data_get($data, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
- $team = data_get($subscription, 'team');
- if ($team) {
- $team->trialEnded();
- }
- $subscription->update([
- 'stripe_subscription_id' => null,
- 'stripe_plan_id' => null,
- 'stripe_cancel_at_period_end' => false,
- 'stripe_invoice_paid' => false,
- 'stripe_trial_already_ended' => false,
- ]);
- // send_internal_notification('customer.subscription.deleted for customer: '.$customerId);
- break;
- case 'customer.subscription.trial_will_end':
- // Not used for now
- $customerId = data_get($data, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
- $team = data_get($subscription, 'team');
- if (! $team) {
- return response('No team found for subscription: '.$subscription->id, 400);
- }
- SubscriptionTrialEndsSoonJob::dispatch($team);
- break;
- case 'customer.subscription.paused':
- $customerId = data_get($data, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
- $team = data_get($subscription, 'team');
- if (! $team) {
- return response('No team found for subscription: '.$subscription->id, 400);
- }
- $team->trialEnded();
- $subscription->update([
- 'stripe_trial_already_ended' => true,
- 'stripe_invoice_paid' => false,
- ]);
- SubscriptionTrialEndedJob::dispatch($team);
- // send_internal_notification('Subscription paused for customer: '.$customerId);
- break;
- default:
- // Unhandled event type
- }
+ return response('Webhook received. Cool cool cool cool cool.', 200);
} catch (Exception $e) {
- if ($type !== 'payment_intent.payment_failed') {
- send_internal_notification("Subscription webhook ($type) failed: ".$e->getMessage());
- }
- $webhook->update([
+ $this->webhook->update([
'status' => 'failed',
'failure_reason' => $e->getMessage(),
]);
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index a0fdd0e97..04e71c4e3 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -140,6 +140,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $buildTarget = null;
+ private bool $disableBuildCache = false;
+
private Collection $saved_outputs;
private ?string $full_healthcheck_url = null;
@@ -166,6 +168,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(int $application_deployment_queue_id)
{
+ $this->onQueue('high');
+
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack');
@@ -176,7 +180,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->pull_request_id = $this->application_deployment_queue->pull_request_id;
$this->commit = $this->application_deployment_queue->commit;
$this->rollback = $this->application_deployment_queue->rollback;
+ $this->disableBuildCache = $this->application->settings->disable_build_cache;
$this->force_rebuild = $this->application_deployment_queue->force_rebuild;
+ if ($this->disableBuildCache) {
+ $this->force_rebuild = true;
+ }
$this->restart_only = $this->application_deployment_queue->restart_only;
$this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile';
$this->only_this_server = $this->application_deployment_queue->only_this_server;
@@ -225,6 +233,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
+ public function tags(): array
+ {
+ return ['server:'.gethostname()];
+ }
+
public function handle(): void
{
$this->application_deployment_queue->update([
@@ -344,8 +357,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function post_deployment()
{
if ($this->server->isProxyShouldRun()) {
- GetContainersStatus::dispatch($this->server)->onQueue('high');
- // dispatch(new ContainerStatusJob($this->server));
+ GetContainersStatus::dispatch($this->server);
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
if ($this->pull_request_id !== 0) {
@@ -457,7 +469,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
$this->save_environment_variables();
if (! is_null($this->env_filename)) {
- $services = collect($composeFile['services']);
+ $services = collect(data_get($composeFile, 'services', []));
$services = $services->map(function ($service, $name) {
$service['env_file'] = [$this->env_filename];
@@ -1318,7 +1330,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function prepare_builder_image()
{
$settings = instanceSettings();
- $helperImage = config('coolify.helper_image');
+ $helperImage = config('constants.coolify.helper_image');
$helperImage = "{$helperImage}:{$settings->helper_version}";
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server);
@@ -1836,7 +1848,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
if ($this->pull_request_id === 0) {
- $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
+ $custom_compose = convertDockerRunToCompose($this->application->custom_docker_run_options);
if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
if (! $this->application->settings->custom_internal_name) {
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
@@ -1970,6 +1982,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->build_args = $this->build_args->implode(' ');
$this->application_deployment_queue->addLogEntry('----------------------------------------');
+ if ($this->disableBuildCache) {
+ $this->application_deployment_queue->addLogEntry('Docker build cache is disabled. It will not be used during the build process.');
+ }
if ($this->application->build_pack === 'static') {
$this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.');
} else {
@@ -1990,22 +2005,11 @@ COPY . .
RUN rm -f /usr/share/nginx/html/nginx.conf
RUN rm -f /usr/share/nginx/html/Dockerfile
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
- $nginx_config = base64_encode('server {
- listen 80;
- listen [::]:80;
- server_name localhost;
-
- location / {
- root /usr/share/nginx/html;
- index index.html;
- try_files $uri $uri.html $uri/index.html $uri/ /index.html =404;
+ if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
+ $nginx_config = base64_encode($this->application->custom_nginx_configuration);
+ } else {
+ $nginx_config = base64_encode(defaultNginxConfiguration());
}
-
- error_page 500 502 503 504 /50x.html;
- location = /50x.html {
- root /usr/share/nginx/html;
- }
- }');
} else {
if ($this->application->build_pack === 'nixpacks') {
$this->nixpacks_plan = base64_encode($this->nixpacks_plan);
@@ -2068,23 +2072,11 @@ WORKDIR /usr/share/nginx/html/
LABEL coolify.deploymentId={$this->deployment_uuid}
COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
-
- $nginx_config = base64_encode('server {
- listen 80;
- listen [::]:80;
- server_name localhost;
-
- location / {
- root /usr/share/nginx/html;
- index index.html;
- try_files $uri $uri.html $uri/index.html $uri/ /index.html =404;
+ if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
+ $nginx_config = base64_encode($this->application->custom_nginx_configuration);
+ } else {
+ $nginx_config = base64_encode(defaultNginxConfiguration());
}
-
- error_page 500 502 503 504 /50x.html;
- location = /50x.html {
- root /usr/share/nginx/html;
- }
- }');
}
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
$base64_build_command = base64_encode($build_command);
@@ -2417,7 +2409,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if (! $this->only_this_server) {
$this->deploy_to_additional_destinations();
}
- $this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
+ //$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
}
}
diff --git a/app/Jobs/ApplicationPullRequestUpdateJob.php b/app/Jobs/ApplicationPullRequestUpdateJob.php
index 2eefc4dd2..ef8e6efb6 100755
--- a/app/Jobs/ApplicationPullRequestUpdateJob.php
+++ b/app/Jobs/ApplicationPullRequestUpdateJob.php
@@ -25,7 +25,9 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue
public ApplicationPreview $preview,
public ProcessStatus $status,
public ?string $deployment_uuid = null
- ) {}
+ ) {
+ $this->onQueue('high');
+ }
public function handle()
{
diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php
index f2348118a..1d3a345e1 100644
--- a/app/Jobs/CheckForUpdatesJob.php
+++ b/app/Jobs/CheckForUpdatesJob.php
@@ -27,7 +27,7 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
$versions = $response->json();
$latest_version = data_get($versions, 'coolify.v4.version');
- $current_version = config('version');
+ $current_version = config('constants.coolify.version');
if (version_compare($latest_version, $current_version, '>')) {
// New version available
diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php
index acbe82338..84f14ed02 100644
--- a/app/Jobs/CleanupInstanceStuffsJob.php
+++ b/app/Jobs/CleanupInstanceStuffsJob.php
@@ -3,14 +3,15 @@
namespace App\Jobs;
use App\Models\TeamInvitation;
-use App\Models\Waitlist;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
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\Facades\Log;
class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue
{
@@ -18,34 +19,21 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho
public function __construct() {}
- // public function uniqueId(): string
- // {
- // return $this->container_name;
- // }
+ public function middleware(): array
+ {
+ return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()];
+ }
public function handle(): void
{
try {
- // $this->cleanup_waitlist();
+ $this->cleanupInvitationLink();
} catch (\Throwable $e) {
- send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage());
- }
- try {
- $this->cleanup_invitation_link();
- } catch (\Throwable $e) {
- send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage());
+ Log::error('CleanupInstanceStuffsJob failed with error: '.$e->getMessage());
}
}
- private function cleanup_waitlist()
- {
- $waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.expiration')))->get();
- foreach ($waitlist as $item) {
- $item->delete();
- }
- }
-
- private function cleanup_invitation_link()
+ private function cleanupInvitationLink()
{
$invitation = TeamInvitation::all();
foreach ($invitation as $item) {
diff --git a/app/Jobs/CoolifyTask.php b/app/Jobs/CoolifyTask.php
index c3692c30b..49a5ba8dd 100755
--- a/app/Jobs/CoolifyTask.php
+++ b/app/Jobs/CoolifyTask.php
@@ -23,7 +23,10 @@ class CoolifyTask implements ShouldBeEncrypted, ShouldQueue
public bool $ignore_errors,
public $call_event_on_finish,
public $call_event_data,
- ) {}
+ ) {
+
+ $this->onQueue('high');
+ }
/**
* Execute the job.
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index fcfe2fe3d..ee702202f 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -60,12 +60,15 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct($backup)
{
+ $this->onQueue('high');
$this->backup = $backup;
}
public function handle(): void
{
try {
+ $databasesToBackup = null;
+
$this->team = Team::find($this->backup->team_id);
if (! $this->team) {
$this->backup->delete();
@@ -197,8 +200,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$databaseType = $this->database->type();
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
}
-
- if (is_null($databasesToBackup)) {
+ if (blank($databasesToBackup)) {
if (str($databaseType)->contains('postgres')) {
$databasesToBackup = [$this->database->postgres_db];
} elseif (str($databaseType)->contains('mongodb')) {
@@ -304,7 +306,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
- $this->team?->notify(new BackupSuccess($this->backup, $this->database, $database));
+ //$this->team?->notify(new BackupSuccess($this->backup, $this->database, $database));
$this->backup_log->update([
'status' => 'success',
'message' => $this->backup_output,
@@ -319,12 +321,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
'filename' => null,
]);
}
- send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database));
}
}
} catch (\Throwable $e) {
- send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
throw $e;
} finally {
if ($this->team) {
@@ -524,7 +524,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
private function getFullImageName(): string
{
$settings = instanceSettings();
- $helperImage = config('coolify.helper_image');
+ $helperImage = config('constants.coolify.helper_image');
$latestVersion = $settings->helper_version;
return "{$helperImage}:{$latestVersion}";
diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php
index 2442d5b06..8b9228e5f 100644
--- a/app/Jobs/DeleteResourceJob.php
+++ b/app/Jobs/DeleteResourceJob.php
@@ -35,7 +35,9 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public bool $deleteVolumes = true,
public bool $dockerCleanup = true,
public bool $deleteConnectedNetworks = true
- ) {}
+ ) {
+ $this->onQueue('high');
+ }
public function handle()
{
@@ -87,7 +89,6 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
$this->resource?->delete_connected_networks($this->resource->uuid);
}
} catch (\Throwable $e) {
- send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e;
} finally {
$this->resource->forceDelete();
diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php
index 900bae99c..80542e03b 100644
--- a/app/Jobs/DockerCleanupJob.php
+++ b/app/Jobs/DockerCleanupJob.php
@@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
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\Facades\Log;
@@ -23,6 +24,11 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public ?string $usageBefore = null;
+ public function middleware(): array
+ {
+ return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
+ }
+
public function __construct(public Server $server, public bool $manualCleanup = false) {}
public function handle(): void
diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php
index a92e44c6b..b92886d38 100644
--- a/app/Jobs/PullHelperImageJob.php
+++ b/app/Jobs/PullHelperImageJob.php
@@ -16,11 +16,14 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000;
- public function __construct(public Server $server) {}
+ public function __construct(public Server $server)
+ {
+ $this->onQueue('high');
+ }
public function handle(): void
{
- $helperImage = config('coolify.helper_image');
+ $helperImage = config('constants.coolify.helper_image');
$latest_version = instanceSettings()->helper_version;
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
}
diff --git a/app/Jobs/PullTemplatesFromCDN.php b/app/Jobs/PullTemplatesFromCDN.php
index bde5e6c7a..45c536e06 100644
--- a/app/Jobs/PullTemplatesFromCDN.php
+++ b/app/Jobs/PullTemplatesFromCDN.php
@@ -17,7 +17,10 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
public $timeout = 10;
- public function __construct() {}
+ public function __construct()
+ {
+ $this->onQueue('high');
+ }
public function handle(): void
{
diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php
index 827641e8c..00575e187 100644
--- a/app/Jobs/ScheduledTaskJob.php
+++ b/app/Jobs/ScheduledTaskJob.php
@@ -2,6 +2,7 @@
namespace App\Jobs;
+use App\Events\ScheduledTaskDone;
use App\Models\Application;
use App\Models\ScheduledTask;
use App\Models\ScheduledTaskExecution;
@@ -19,7 +20,7 @@ class ScheduledTaskJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public ?Team $team = null;
+ public Team $team;
public Server $server;
@@ -39,6 +40,8 @@ class ScheduledTaskJob implements ShouldQueue
public function __construct($task)
{
+ $this->onQueue('high');
+
$this->task = $task;
if ($service = $task->service()->first()) {
$this->resource = $service;
@@ -47,7 +50,7 @@ class ScheduledTaskJob implements ShouldQueue
} else {
throw new \RuntimeException('ScheduledTaskJob failed: No resource found.');
}
- $this->team = Team::find($task->team_id);
+ $this->team = Team::findOrFail($task->team_id);
$this->server_timezone = $this->getServerTimezone();
}
@@ -125,6 +128,7 @@ class ScheduledTaskJob implements ShouldQueue
// send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
throw $e;
} finally {
+ ScheduledTaskDone::dispatch($this->team->id);
}
}
}
diff --git a/app/Jobs/SendMessageToDiscordJob.php b/app/Jobs/SendMessageToDiscordJob.php
index 5b406f50f..99aeaeea2 100644
--- a/app/Jobs/SendMessageToDiscordJob.php
+++ b/app/Jobs/SendMessageToDiscordJob.php
@@ -32,7 +32,9 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(
public DiscordMessage $message,
public string $webhookUrl
- ) {}
+ ) {
+ $this->onQueue('high');
+ }
/**
* Execute the job.
diff --git a/app/Jobs/SendMessageToSlackJob.php b/app/Jobs/SendMessageToSlackJob.php
new file mode 100644
index 000000000..470002d23
--- /dev/null
+++ b/app/Jobs/SendMessageToSlackJob.php
@@ -0,0 +1,59 @@
+onQueue('high');
+ }
+
+ public function handle(): void
+ {
+ Http::post($this->webhookUrl, [
+ 'blocks' => [
+ [
+ 'type' => 'section',
+ 'text' => [
+ 'type' => 'plain_text',
+ 'text' => 'Coolify Notification',
+ ],
+ ],
+ ],
+ 'attachments' => [
+ [
+ 'color' => $this->message->color,
+ 'blocks' => [
+ [
+ 'type' => 'header',
+ 'text' => [
+ 'type' => 'plain_text',
+ 'text' => $this->message->title,
+ ],
+ ],
+ [
+ 'type' => 'section',
+ 'text' => [
+ 'type' => 'mrkdwn',
+ 'text' => $this->message->description,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
+ }
+}
diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php
index bf52b782f..85f4fc934 100644
--- a/app/Jobs/SendMessageToTelegramJob.php
+++ b/app/Jobs/SendMessageToTelegramJob.php
@@ -33,7 +33,9 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
public string $token,
public string $chatId,
public ?string $topicId = null,
- ) {}
+ ) {
+ $this->onQueue('high');
+ }
/**
* Execute the job.
@@ -70,7 +72,7 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
}
$response = Http::post($url, $payload);
if ($response->failed()) {
- throw new \Exception('Telegram notification failed with '.$response->status().' status code.'.$response->body());
+ throw new \RuntimeException('Telegram notification failed with '.$response->status().' status code.'.$response->body());
}
}
}
diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php
index 449a2da14..49d8dfe08 100644
--- a/app/Jobs/ServerCheckJob.php
+++ b/app/Jobs/ServerCheckJob.php
@@ -13,6 +13,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
@@ -25,7 +26,17 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public $containers;
- public function __construct(public Server $server) {}
+ public function middleware(): array
+ {
+ return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
+ }
+
+ public function __construct(public Server $server)
+ {
+ if (isDev()) {
+ $this->handle();
+ }
+ }
public function handle()
{
diff --git a/app/Jobs/ServerCheckNewJob.php b/app/Jobs/ServerCheckNewJob.php
index 9ce52759c..3e8e60a31 100644
--- a/app/Jobs/ServerCheckNewJob.php
+++ b/app/Jobs/ServerCheckNewJob.php
@@ -2,6 +2,7 @@
namespace App\Jobs;
+use App\Actions\Server\ResourcesCheck;
use App\Actions\Server\ServerCheck;
use App\Models\Server;
use Illuminate\Bus\Queueable;
@@ -25,6 +26,7 @@ class ServerCheckNewJob implements ShouldBeEncrypted, ShouldQueue
{
try {
ServerCheck::run($this->server);
+ ResourcesCheck::dispatch($this->server);
} catch (\Throwable $e) {
return handleError($e);
}
diff --git a/app/Jobs/ServerFilesFromServerJob.php b/app/Jobs/ServerFilesFromServerJob.php
index 769dfc004..58455df2f 100644
--- a/app/Jobs/ServerFilesFromServerJob.php
+++ b/app/Jobs/ServerFilesFromServerJob.php
@@ -16,7 +16,10 @@ class ServerFilesFromServerJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(public ServiceApplication|ServiceDatabase|Application $resource) {}
+ public function __construct(public ServiceApplication|ServiceDatabase|Application $resource)
+ {
+ $this->onQueue('high');
+ }
public function handle()
{
diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php
index 084b6bf81..aa82c6dad 100644
--- a/app/Jobs/ServerLimitCheckJob.php
+++ b/app/Jobs/ServerLimitCheckJob.php
@@ -30,8 +30,7 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
try {
$servers = $this->team->servers;
$servers_count = $servers->count();
- $limit = data_get($this->team->limits, 'serverLimit', 2);
- $number_of_servers_to_disable = $servers_count - $limit;
+ $number_of_servers_to_disable = $servers_count - $this->team->limits;
if ($number_of_servers_to_disable > 0) {
$servers = $servers->sortbyDesc('created_at');
$servers_to_disable = $servers->take($number_of_servers_to_disable);
diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php
index 0723ffcee..9a8d86be1 100644
--- a/app/Jobs/ServerStorageCheckJob.php
+++ b/app/Jobs/ServerStorageCheckJob.php
@@ -25,7 +25,7 @@ class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue
return isDev() ? 1 : 3;
}
- public function __construct(public Server $server, public ?int $percentage = null) {}
+ public function __construct(public Server $server, public int|string|null $percentage = null) {}
public function handle()
{
diff --git a/app/Jobs/ServerStorageSaveJob.php b/app/Jobs/ServerStorageSaveJob.php
index 526cd5375..17a293f94 100644
--- a/app/Jobs/ServerStorageSaveJob.php
+++ b/app/Jobs/ServerStorageSaveJob.php
@@ -14,7 +14,10 @@ class ServerStorageSaveJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(public LocalFileVolume $localFileVolume) {}
+ public function __construct(public LocalFileVolume $localFileVolume)
+ {
+ $this->onQueue('high');
+ }
public function handle()
{
diff --git a/app/Jobs/StripeProcessJob.php b/app/Jobs/StripeProcessJob.php
new file mode 100644
index 000000000..00c9b6d18
--- /dev/null
+++ b/app/Jobs/StripeProcessJob.php
@@ -0,0 +1,246 @@
+onQueue('high');
+ }
+
+ public function handle(): void
+ {
+ try {
+ $excludedPlans = config('subscription.stripe_excluded_plans');
+
+ $type = data_get($this->event, 'type');
+ $this->type = $type;
+ $data = data_get($this->event, 'data.object');
+ switch ($type) {
+ case 'radar.early_fraud_warning.created':
+ $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
+ $id = data_get($data, 'id');
+ $charge = data_get($data, 'charge');
+ if ($charge) {
+ $stripe->refunds->create(['charge' => $charge]);
+ }
+ $pi = data_get($data, 'payment_intent');
+ $piData = $stripe->paymentIntents->retrieve($pi, []);
+ $customerId = data_get($piData, 'customer');
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if ($subscription) {
+ $subscriptionId = data_get($subscription, 'stripe_subscription_id');
+ $stripe->subscriptions->cancel($subscriptionId, []);
+ $subscription->update([
+ 'stripe_invoice_paid' => false,
+ ]);
+ send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
+ } else {
+ send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
+ throw new \RuntimeException("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
+ }
+ break;
+ case 'checkout.session.completed':
+ $clientReferenceId = data_get($data, 'client_reference_id');
+ if (is_null($clientReferenceId)) {
+ send_internal_notification('Checkout session completed without client reference id.');
+ break;
+ }
+ $userId = Str::before($clientReferenceId, ':');
+ $teamId = Str::after($clientReferenceId, ':');
+ $subscriptionId = data_get($data, 'subscription');
+ $customerId = data_get($data, 'customer');
+ $team = Team::find($teamId);
+ $found = $team->members->where('id', $userId)->first();
+ if (! $found->isAdmin()) {
+ send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
+ throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
+ }
+ $subscription = Subscription::where('team_id', $teamId)->first();
+ if ($subscription) {
+ send_internal_notification('Old subscription activated for team: '.$teamId);
+ $subscription->update([
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => true,
+ ]);
+ } else {
+ send_internal_notification('New subscription for team: '.$teamId);
+ Subscription::create([
+ 'team_id' => $teamId,
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => true,
+ ]);
+ }
+ break;
+ case 'invoice.paid':
+ $customerId = data_get($data, 'customer');
+ $planId = data_get($data, 'lines.data.0.plan.id');
+ if (Str::contains($excludedPlans, $planId)) {
+ send_internal_notification('Subscription excluded.');
+ break;
+ }
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if ($subscription) {
+ $subscription->update([
+ 'stripe_invoice_paid' => true,
+ ]);
+ } else {
+ throw new \RuntimeException("No subscription found for customer: {$customerId}");
+ }
+ break;
+ case 'invoice.payment_failed':
+ $customerId = data_get($data, 'customer');
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if (! $subscription) {
+ send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
+ throw new \RuntimeException("No subscription found for customer: {$customerId}");
+ }
+ $team = data_get($subscription, 'team');
+ if (! $team) {
+ send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
+ throw new \RuntimeException("No team found in Coolify for customer: {$customerId}");
+ }
+ if (! $subscription->stripe_invoice_paid) {
+ SubscriptionInvoiceFailedJob::dispatch($team);
+ send_internal_notification('Invoice payment failed: '.$customerId);
+ } else {
+ send_internal_notification('Invoice payment failed but already paid: '.$customerId);
+ }
+ break;
+ case 'payment_intent.payment_failed':
+ $customerId = data_get($data, 'customer');
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if (! $subscription) {
+ send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
+ throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}");
+ }
+ if ($subscription->stripe_invoice_paid) {
+ send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
+
+ return;
+ }
+ send_internal_notification('Subscription payment failed for customer: '.$customerId);
+ break;
+ case 'customer.subscription.created':
+ $customerId = data_get($data, 'customer');
+ $subscriptionId = data_get($data, 'id');
+ $teamId = data_get($data, 'metadata.team_id');
+ $userId = data_get($data, 'metadata.user_id');
+ if (! $teamId || ! $userId) {
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if ($subscription) {
+ throw new \RuntimeException("Subscription already exists for customer: {$customerId}");
+ }
+ throw new \RuntimeException('No team id or user id found');
+ }
+ $team = Team::find($teamId);
+ $found = $team->members->where('id', $userId)->first();
+ if (! $found->isAdmin()) {
+ send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
+ throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
+ }
+ $subscription = Subscription::where('team_id', $teamId)->first();
+ if ($subscription) {
+ send_internal_notification("Subscription already exists for team: {$teamId}");
+ throw new \RuntimeException("Subscription already exists for team: {$teamId}");
+ } else {
+ Subscription::create([
+ 'team_id' => $teamId,
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => false,
+ ]);
+ }
+ case 'customer.subscription.updated':
+ $teamId = data_get($data, 'metadata.team_id');
+ $userId = data_get($data, 'metadata.user_id');
+ $customerId = data_get($data, 'customer');
+ $status = data_get($data, 'status');
+ $subscriptionId = data_get($data, 'items.data.0.subscription');
+ $planId = data_get($data, 'items.data.0.plan.id');
+ if (Str::contains($excludedPlans, $planId)) {
+ send_internal_notification('Subscription excluded.');
+ break;
+ }
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if (! $subscription) {
+ if ($status === 'incomplete_expired') {
+ send_internal_notification('Subscription incomplete expired');
+ throw new \RuntimeException('Subscription incomplete expired');
+ }
+ if ($teamId) {
+ $subscription = Subscription::create([
+ 'team_id' => $teamId,
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => false,
+ ]);
+ } else {
+ send_internal_notification('No subscription and team id found');
+ throw new \RuntimeException('No subscription and team id found');
+ }
+ }
+ $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
+ $feedback = data_get($data, 'cancellation_details.feedback');
+ $comment = data_get($data, 'cancellation_details.comment');
+ $lookup_key = data_get($data, 'items.data.0.price.lookup_key');
+ if (str($lookup_key)->contains('dynamic')) {
+ $quantity = data_get($data, 'items.data.0.quantity', 2);
+ $team = data_get($subscription, 'team');
+ if ($team) {
+ $team->update([
+ 'custom_server_limit' => $quantity,
+ ]);
+ }
+ ServerLimitCheckJob::dispatch($team);
+ }
+ $subscription->update([
+ 'stripe_feedback' => $feedback,
+ 'stripe_comment' => $comment,
+ 'stripe_plan_id' => $planId,
+ 'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
+ ]);
+ if ($status === 'paused' || $status === 'incomplete_expired') {
+ $subscription->update([
+ 'stripe_invoice_paid' => false,
+ ]);
+ }
+ if ($feedback) {
+ $reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
+ if ($comment) {
+ $reason .= ' with comment: \''.$comment."'";
+ }
+ }
+ break;
+ case 'customer.subscription.deleted':
+ // End subscription
+ $customerId = data_get($data, 'customer');
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
+ $team = data_get($subscription, 'team');
+ $team?->subscriptionEnded();
+ break;
+ default:
+ throw new \RuntimeException("Unhandled event type: {$type}");
+ }
+ } catch (\Exception $e) {
+ send_internal_notification('StripeProcessJob error: '.$e->getMessage());
+ }
+ }
+}
diff --git a/app/Jobs/SubscriptionInvoiceFailedJob.php b/app/Jobs/SubscriptionInvoiceFailedJob.php
index aabeecef5..dc511f445 100755
--- a/app/Jobs/SubscriptionInvoiceFailedJob.php
+++ b/app/Jobs/SubscriptionInvoiceFailedJob.php
@@ -15,7 +15,10 @@ class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(protected Team $team) {}
+ public function __construct(protected Team $team)
+ {
+ $this->onQueue('high');
+ }
public function handle()
{
diff --git a/app/Jobs/SubscriptionTrialEndedJob.php b/app/Jobs/SubscriptionTrialEndedJob.php
deleted file mode 100755
index 88a5e06be..000000000
--- a/app/Jobs/SubscriptionTrialEndedJob.php
+++ /dev/null
@@ -1,42 +0,0 @@
-team);
- $mail = new MailMessage;
- $mail->subject('Action required: You trial in Coolify Cloud ended.');
- $mail->view('emails.trial-ended', [
- 'stripeCustomerPortal' => $session->url,
- ]);
- $this->team->members()->each(function ($member) use ($mail) {
- if ($member->isAdmin()) {
- send_user_an_email($mail, $member->email);
- send_internal_notification('Trial reminder email sent to '.$member->email);
- }
- });
- } catch (\Throwable $e) {
- send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage());
- throw $e;
- }
- }
-}
diff --git a/app/Jobs/SubscriptionTrialEndsSoonJob.php b/app/Jobs/SubscriptionTrialEndsSoonJob.php
deleted file mode 100755
index 2a76a1097..000000000
--- a/app/Jobs/SubscriptionTrialEndsSoonJob.php
+++ /dev/null
@@ -1,42 +0,0 @@
-team);
- $mail = new MailMessage;
- $mail->subject('You trial in Coolify Cloud ends soon.');
- $mail->view('emails.trial-ends-soon', [
- 'stripeCustomerPortal' => $session->url,
- ]);
- $this->team->members()->each(function ($member) use ($mail) {
- if ($member->isAdmin()) {
- send_user_an_email($mail, $member->email);
- send_internal_notification('Trial reminder email sent to '.$member->email);
- }
- });
- } catch (\Throwable $e) {
- send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage());
- throw $e;
- }
- }
-}
diff --git a/app/Jobs/UpdateCoolifyJob.php b/app/Jobs/UpdateCoolifyJob.php
index 1e5197b6f..f0e43cbc0 100644
--- a/app/Jobs/UpdateCoolifyJob.php
+++ b/app/Jobs/UpdateCoolifyJob.php
@@ -18,6 +18,11 @@ class UpdateCoolifyJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 600;
+ public function __construct()
+ {
+ $this->onQueue('high');
+ }
+
public function handle(): void
{
try {
diff --git a/app/Listeners/ProxyStartedNotification.php b/app/Listeners/ProxyStartedNotification.php
index d0541b162..9045b1e5c 100644
--- a/app/Listeners/ProxyStartedNotification.php
+++ b/app/Listeners/ProxyStartedNotification.php
@@ -14,7 +14,7 @@ class ProxyStartedNotification
public function handle(ProxyStarted $event): void
{
$this->server = data_get($event, 'data');
- $this->server->setupDefault404Redirect();
+ $this->server->setupDefaultRedirect();
$this->server->setupDynamicProxyConfiguration();
$this->server->proxy->force_stop = false;
$this->server->save();
diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php
index 7078a21e9..359db6329 100644
--- a/app/Livewire/Admin/Index.php
+++ b/app/Livewire/Admin/Index.php
@@ -2,8 +2,10 @@
namespace App\Livewire\Admin;
+use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
@@ -23,7 +25,7 @@ class Index extends Component
return redirect()->route('dashboard');
}
- if (auth()->user()->id !== 0) {
+ if (Auth::id() !== 0) {
return redirect()->route('dashboard');
}
$this->getSubscribers();
@@ -41,23 +43,19 @@ class Index extends Component
public function getSubscribers()
{
- $this->inactiveSubscribers = User::whereDoesntHave('teams', function ($query) {
- $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
- })->count();
- $this->activeSubscribers = User::whereHas('teams', function ($query) {
- $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
- })->count();
+ $this->inactiveSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', false)->count();
+ $this->activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->count();
}
public function switchUser(int $user_id)
{
- if (auth()->user()->id !== 0) {
+ if (Auth::id() !== 0) {
return redirect()->route('dashboard');
}
$user = User::find($user_id);
$team_to_switch_to = $user->teams->first();
Cache::forget("team:{$user->id}");
- auth()->login($user);
+ Auth::login($user);
refreshSession($team_to_switch_to);
return redirect(request()->header('Referer'));
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index c9c3092b3..eadabba7c 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -66,11 +66,15 @@ class Index extends Component
public bool $serverReachable = true;
+ public ?string $minDockerVersion = null;
+
public function mount()
{
if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) {
return redirect()->route('dashboard');
}
+
+ $this->minDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.');
$this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name();
if (isDev()) {
@@ -168,13 +172,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function getProxyType()
{
- // Set Default Proxy Type
$this->selectProxy(ProxyTypes::TRAEFIK->value);
- // $proxyTypeSet = $this->createdServer->proxy->type;
- // if (!$proxyTypeSet) {
- // $this->currentState = 'select-proxy';
- // return;
- // }
$this->getProjects();
}
@@ -185,7 +183,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
return;
}
- $this->createdPrivateKey = PrivateKey::find($this->selectedExistingPrivateKey);
+ $this->createdPrivateKey = PrivateKey::where('team_id', currentTeam()->id)->where('id', $this->selectedExistingPrivateKey)->first();
$this->privateKey = $this->createdPrivateKey->private_key;
$this->currentState = 'create-server';
}
diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php
index 1ef8761fa..337f1d067 100644
--- a/app/Livewire/Destination/New/Docker.php
+++ b/app/Livewire/Destination/New/Docker.php
@@ -6,7 +6,7 @@ use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Livewire\Attributes\Locked;
-use Livewire\Attributes\Rule;
+use Livewire\Attributes\Validate;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -18,16 +18,16 @@ class Docker extends Component
#[Locked]
public Server $selectedServer;
- #[Rule(['required', 'string'])]
+ #[Validate(['required', 'string'])]
public string $name;
- #[Rule(['required', 'string'])]
+ #[Validate(['required', 'string'])]
public string $network;
- #[Rule(['required', 'string'])]
+ #[Validate(['required', 'string'])]
public string $serverId;
- #[Rule(['required', 'boolean'])]
+ #[Validate(['required', 'boolean'])]
public bool $isSwarm = false;
public function mount(?string $server_id = null)
@@ -35,9 +35,11 @@ class Docker extends Component
$this->network = new Cuid2;
$this->servers = Server::isUsable()->get();
if ($server_id) {
- $this->selectedServer = $this->servers->find($server_id);
+ $this->selectedServer = $this->servers->find($server_id) ?: $this->servers->first();
+ $this->serverId = $this->selectedServer->id;
} else {
$this->selectedServer = $this->servers->first();
+ $this->serverId = $this->selectedServer->id;
}
$this->generateName();
}
diff --git a/app/Livewire/Destination/Show.php b/app/Livewire/Destination/Show.php
index f75749382..5c4d6c170 100644
--- a/app/Livewire/Destination/Show.php
+++ b/app/Livewire/Destination/Show.php
@@ -6,7 +6,7 @@ use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Livewire\Attributes\Locked;
-use Livewire\Attributes\Rule;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Show extends Component
@@ -14,13 +14,13 @@ class Show extends Component
#[Locked]
public $destination;
- #[Rule(['string', 'required'])]
+ #[Validate(['string', 'required'])]
public string $name;
- #[Rule(['string', 'required'])]
+ #[Validate(['string', 'required'])]
public string $network;
- #[Rule(['string', 'required'])]
+ #[Validate(['string', 'required'])]
public string $serverIp;
public function mount(string $destination_uuid)
diff --git a/app/Livewire/Dev/Compose.php b/app/Livewire/Dev/Compose.php
deleted file mode 100644
index a5cd53fc2..000000000
--- a/app/Livewire/Dev/Compose.php
+++ /dev/null
@@ -1,37 +0,0 @@
-services = get_service_templates();
- }
-
- public function setService(string $selected)
- {
- $this->base64 = data_get($this->services, $selected.'.compose');
- if ($this->base64) {
- $this->compose = base64_decode($this->base64);
- }
- }
-
- public function updatedCompose($value)
- {
- $this->base64 = base64_encode($value);
- }
-
- public function render()
- {
- return view('livewire.dev.compose');
- }
-}
diff --git a/app/Livewire/Help.php b/app/Livewire/Help.php
index b174b1429..f51527fbe 100644
--- a/app/Livewire/Help.php
+++ b/app/Livewire/Help.php
@@ -5,17 +5,17 @@ namespace App\Livewire;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Http;
-use Livewire\Attributes\Rule;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Help extends Component
{
use WithRateLimiting;
- #[Rule(['required', 'min:10', 'max:1000'])]
+ #[Validate(['required', 'min:10', 'max:1000'])]
public string $description;
- #[Rule(['required', 'min:3'])]
+ #[Validate(['required', 'min:3'])]
public string $subject;
public function submit()
diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php
index 10ba0c86a..e97cceb0d 100644
--- a/app/Livewire/NavbarDeleteTeam.php
+++ b/app/Livewire/NavbarDeleteTeam.php
@@ -31,7 +31,7 @@ class NavbarDeleteTeam extends Component
$currentTeam->delete();
$currentTeam->members->each(function ($user) use ($currentTeam) {
- if ($user->id === auth()->user()->id) {
+ if ($user->id === Auth::id()) {
return;
}
$user->teams()->detach($currentTeam);
diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php
index df5489a24..7a177a227 100644
--- a/app/Livewire/Notifications/Discord.php
+++ b/app/Livewire/Notifications/Discord.php
@@ -4,35 +4,35 @@ namespace App\Livewire\Notifications;
use App\Models\Team;
use App\Notifications\Test;
-use Livewire\Attributes\Rule;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Discord extends Component
{
public Team $team;
- #[Rule(['boolean'])]
+ #[Validate(['boolean'])]
public bool $discordEnabled = false;
- #[Rule(['url', 'nullable'])]
+ #[Validate(['url', 'nullable'])]
public ?string $discordWebhookUrl = null;
- #[Rule(['boolean'])]
+ #[Validate(['boolean'])]
public bool $discordNotificationsTest = false;
- #[Rule(['boolean'])]
+ #[Validate(['boolean'])]
public bool $discordNotificationsDeployments = false;
- #[Rule(['boolean'])]
+ #[Validate(['boolean'])]
public bool $discordNotificationsStatusChanges = false;
- #[Rule(['boolean'])]
+ #[Validate(['boolean'])]
public bool $discordNotificationsDatabaseBackups = false;
- #[Rule(['boolean'])]
+ #[Validate(['boolean'])]
public bool $discordNotificationsScheduledTasks = false;
- #[Rule(['boolean'])]
+ #[Validate(['boolean'])]
public bool $discordNotificationsServerDiskUsage = false;
public function mount()
@@ -41,7 +41,7 @@ class Discord extends Component
$this->team = auth()->user()->currentTeam();
$this->syncData();
} catch (\Throwable $e) {
- handleError($e, $this);
+ return handleError($e, $this);
}
}
@@ -57,11 +57,8 @@ class Discord extends Component
$this->team->discord_notifications_database_backups = $this->discordNotificationsDatabaseBackups;
$this->team->discord_notifications_scheduled_tasks = $this->discordNotificationsScheduledTasks;
$this->team->discord_notifications_server_disk_usage = $this->discordNotificationsServerDiskUsage;
- try {
- $this->saveModel();
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
+ $this->team->save();
+ refreshSession();
} else {
$this->discordEnabled = $this->team->discord_enabled;
$this->discordWebhookUrl = $this->team->discord_webhook_url;
@@ -74,6 +71,22 @@ class Discord extends Component
}
}
+ public function instantSaveDiscordEnabled()
+ {
+ try {
+ $this->validate([
+ 'discordWebhookUrl' => 'required',
+ ], [
+ 'discordWebhookUrl.required' => 'Discord Webhook URL is required.',
+ ]);
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ $this->discordEnabled = false;
+
+ return handleError($e, $this);
+ }
+ }
+
public function instantSave()
{
try {
@@ -96,7 +109,7 @@ class Discord extends Component
public function saveModel()
{
- $this->team->save();
+ $this->syncData(true);
refreshSession();
$this->dispatch('success', 'Settings saved.');
}
diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php
index 08415731d..ab3768643 100644
--- a/app/Livewire/Notifications/Email.php
+++ b/app/Livewire/Notifications/Email.php
@@ -5,83 +5,151 @@ namespace App\Livewire\Notifications;
use App\Models\Team;
use App\Notifications\Test;
use Illuminate\Support\Facades\RateLimiter;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Email extends Component
{
public Team $team;
+ #[Locked]
public string $emails;
- public bool $sharedEmailEnabled = false;
+ #[Validate(['boolean'])]
+ public bool $smtpEnabled = false;
- protected $rules = [
- '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.smtp_notifications_scheduled_tasks' => 'nullable|boolean',
- 'team.smtp_notifications_server_disk_usage' => 'nullable|boolean',
- 'team.use_instance_email_settings' => 'boolean',
- 'team.resend_enabled' => 'nullable|boolean',
- 'team.resend_api_key' => 'nullable',
- ];
+ #[Validate(['boolean'])]
+ public bool $useInstanceEmailSettings = false;
- protected $validationAttributes = [
- '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',
- ];
+ #[Validate(['nullable', 'email'])]
+ public ?string $smtpFromAddress = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpFromName = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpRecipients = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpHost = null;
+
+ #[Validate(['nullable', 'numeric'])]
+ public ?int $smtpPort = null;
+
+ #[Validate(['nullable', 'string', 'in:tls,ssl,none'])]
+ public ?string $smtpEncryption = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpUsername = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $smtpPassword = null;
+
+ #[Validate(['nullable', 'numeric'])]
+ public ?int $smtpTimeout = null;
+
+ #[Validate(['boolean'])]
+ public bool $smtpNotificationsTest = false;
+
+ #[Validate(['boolean'])]
+ public bool $smtpNotificationsDeployments = false;
+
+ #[Validate(['boolean'])]
+ public bool $smtpNotificationsStatusChanges = false;
+
+ #[Validate(['boolean'])]
+ public bool $smtpNotificationsDatabaseBackups = false;
+
+ #[Validate(['boolean'])]
+ public bool $smtpNotificationsScheduledTasks = false;
+
+ #[Validate(['boolean'])]
+ public bool $smtpNotificationsServerDiskUsage = false;
+
+ #[Validate(['boolean'])]
+ public bool $resendEnabled;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $resendApiKey = null;
+
+ #[Validate(['nullable', 'email'])]
+ public ?string $testEmailAddress = null;
public function mount()
- {
- $this->team = auth()->user()->currentTeam();
- ['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits;
- $this->emails = auth()->user()->email;
- }
-
- public function submitFromFields()
{
try {
- $this->resetErrorBag();
- $this->validate([
- 'team.smtp_from_address' => 'required|email',
- 'team.smtp_from_name' => 'required',
- ]);
- $this->team->save();
- refreshSession();
- $this->dispatch('success', 'Settings saved.');
+ $this->team = auth()->user()->currentTeam();
+ $this->emails = auth()->user()->email;
+ $this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
- public function sendTestNotification()
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->team->smtp_enabled = $this->smtpEnabled;
+ $this->team->smtp_from_address = $this->smtpFromAddress;
+ $this->team->smtp_from_name = $this->smtpFromName;
+ $this->team->smtp_host = $this->smtpHost;
+ $this->team->smtp_port = $this->smtpPort;
+ $this->team->smtp_encryption = $this->smtpEncryption;
+ $this->team->smtp_username = $this->smtpUsername;
+ $this->team->smtp_password = $this->smtpPassword;
+ $this->team->smtp_timeout = $this->smtpTimeout;
+ $this->team->smtp_recipients = $this->smtpRecipients;
+ $this->team->smtp_notifications_test = $this->smtpNotificationsTest;
+ $this->team->smtp_notifications_deployments = $this->smtpNotificationsDeployments;
+ $this->team->smtp_notifications_status_changes = $this->smtpNotificationsStatusChanges;
+ $this->team->smtp_notifications_database_backups = $this->smtpNotificationsDatabaseBackups;
+ $this->team->smtp_notifications_scheduled_tasks = $this->smtpNotificationsScheduledTasks;
+ $this->team->smtp_notifications_server_disk_usage = $this->smtpNotificationsServerDiskUsage;
+ $this->team->use_instance_email_settings = $this->useInstanceEmailSettings;
+ $this->team->resend_enabled = $this->resendEnabled;
+ $this->team->resend_api_key = $this->resendApiKey;
+ $this->team->save();
+ refreshSession();
+ } else {
+ $this->smtpEnabled = $this->team->smtp_enabled;
+ $this->smtpFromAddress = $this->team->smtp_from_address;
+ $this->smtpFromName = $this->team->smtp_from_name;
+ $this->smtpHost = $this->team->smtp_host;
+ $this->smtpPort = $this->team->smtp_port;
+ $this->smtpEncryption = $this->team->smtp_encryption;
+ $this->smtpUsername = $this->team->smtp_username;
+ $this->smtpPassword = $this->team->smtp_password;
+ $this->smtpTimeout = $this->team->smtp_timeout;
+ $this->smtpRecipients = $this->team->smtp_recipients;
+ $this->smtpNotificationsTest = $this->team->smtp_notifications_test;
+ $this->smtpNotificationsDeployments = $this->team->smtp_notifications_deployments;
+ $this->smtpNotificationsStatusChanges = $this->team->smtp_notifications_status_changes;
+ $this->smtpNotificationsDatabaseBackups = $this->team->smtp_notifications_database_backups;
+ $this->smtpNotificationsScheduledTasks = $this->team->smtp_notifications_scheduled_tasks;
+ $this->smtpNotificationsServerDiskUsage = $this->team->smtp_notifications_server_disk_usage;
+ $this->useInstanceEmailSettings = $this->team->use_instance_email_settings;
+ $this->resendEnabled = $this->team->resend_enabled;
+ $this->resendApiKey = $this->team->resend_api_key;
+ }
+ }
+
+ public function sendTestEmail()
{
try {
+ $this->validate([
+ 'testEmailAddress' => 'required|email',
+ ], [
+ 'testEmailAddress.required' => 'Test email address is required.',
+ 'testEmailAddress.email' => 'Please enter a valid email address.',
+ ]);
+
$executed = RateLimiter::attempt(
'test-email:'.$this->team->id,
$perMinute = 0,
function () {
- $this->team?->notify(new Test($this->emails));
+ $this->team?->notify(new Test($this->testEmailAddress));
$this->dispatch('success', 'Test Email sent.');
},
$decaySeconds = 10,
@@ -98,38 +166,45 @@ class Email extends Component
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();
- refreshSession();
- $this->dispatch('success', 'Settings saved.');
+ $this->smtpEnabled = false;
+ $this->resendEnabled = false;
+ $this->saveModel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
+ public function instantSaveSmtpEnabled()
+ {
+ try {
+ $this->validate([
+ 'smtpHost' => 'required',
+ 'smtpPort' => 'required|numeric',
+ ], [
+ 'smtpHost.required' => 'SMTP Host is required.',
+ 'smtpPort.required' => 'SMTP Port is required.',
+ ]);
+ $this->resendEnabled = false;
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ $this->smtpEnabled = false;
+
+ return handleError($e, $this);
+ }
+ }
+
public function instantSaveResend()
{
try {
- $this->team->smtp_enabled = false;
- $this->submitResend();
+ $this->validate([
+ 'resendApiKey' => 'required',
+ ], [
+ 'resendApiKey.required' => 'Resend API Key is required.',
+ ]);
+ $this->smtpEnabled = false;
+ $this->saveModel();
} catch (\Throwable $e) {
- $this->team->smtp_enabled = false;
-
- return handleError($e, $this);
- }
- }
-
- public function instantSave()
- {
- try {
- $this->team->resend_enabled = false;
- $this->submit();
- } catch (\Throwable $e) {
- $this->team->smtp_enabled = false;
+ $this->resendEnabled = false;
return handleError($e, $this);
}
@@ -137,7 +212,7 @@ class Email extends Component
public function saveModel()
{
- $this->team->save();
+ $this->syncData(true);
refreshSession();
$this->dispatch('success', 'Settings saved.');
}
@@ -146,43 +221,8 @@ class Email extends Component
{
try {
$this->resetErrorBag();
- if (! $this->team->use_instance_email_settings) {
- $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();
- refreshSession();
- $this->dispatch('success', 'Settings saved.');
+ $this->saveModel();
} catch (\Throwable $e) {
- $this->team->smtp_enabled = false;
-
- return handleError($e, $this);
- }
- }
-
- public function submitResend()
- {
- try {
- $this->resetErrorBag();
- $this->validate([
- 'team.smtp_from_address' => 'required|email',
- 'team.smtp_from_name' => 'required',
- 'team.resend_api_key' => 'required',
- ]);
- $this->team->save();
- refreshSession();
- $this->dispatch('success', 'Settings saved.');
- } catch (\Throwable $e) {
- $this->team->resend_enabled = false;
-
return handleError($e, $this);
}
}
@@ -190,35 +230,28 @@ class Email extends Component
public function copyFromInstanceSettings()
{
$settings = instanceSettings();
+
if ($settings->smtp_enabled) {
- $team = currentTeam();
- $team->update([
- 'smtp_enabled' => $settings->smtp_enabled,
- 'smtp_from_address' => $settings->smtp_from_address,
- 'smtp_from_name' => $settings->smtp_from_name,
- 'smtp_recipients' => $settings->smtp_recipients,
- 'smtp_host' => $settings->smtp_host,
- 'smtp_port' => $settings->smtp_port,
- 'smtp_encryption' => $settings->smtp_encryption,
- 'smtp_username' => $settings->smtp_username,
- 'smtp_password' => $settings->smtp_password,
- 'smtp_timeout' => $settings->smtp_timeout,
- ]);
- refreshSession();
- $this->team = $team;
- $this->dispatch('success', 'Settings saved.');
+ $this->smtpEnabled = true;
+ $this->smtpFromAddress = $settings->smtp_from_address;
+ $this->smtpFromName = $settings->smtp_from_name;
+ $this->smtpRecipients = $settings->smtp_recipients;
+ $this->smtpHost = $settings->smtp_host;
+ $this->smtpPort = $settings->smtp_port;
+ $this->smtpEncryption = $settings->smtp_encryption;
+ $this->smtpUsername = $settings->smtp_username;
+ $this->smtpPassword = $settings->smtp_password;
+ $this->smtpTimeout = $settings->smtp_timeout;
+ $this->resendEnabled = false;
+ $this->saveModel();
return;
}
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->dispatch('success', 'Settings saved.');
+ $this->resendEnabled = true;
+ $this->resendApiKey = $settings->resend_api_key;
+ $this->smtpEnabled = false;
+ $this->saveModel();
return;
}
diff --git a/app/Livewire/Notifications/Slack.php b/app/Livewire/Notifications/Slack.php
new file mode 100644
index 000000000..06b7643ea
--- /dev/null
+++ b/app/Livewire/Notifications/Slack.php
@@ -0,0 +1,131 @@
+team = auth()->user()->currentTeam();
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->team->slack_enabled = $this->slackEnabled;
+ $this->team->slack_webhook_url = $this->slackWebhookUrl;
+ $this->team->slack_notifications_test = $this->slackNotificationsTest;
+ $this->team->slack_notifications_deployments = $this->slackNotificationsDeployments;
+ $this->team->slack_notifications_status_changes = $this->slackNotificationsStatusChanges;
+ $this->team->slack_notifications_database_backups = $this->slackNotificationsDatabaseBackups;
+ $this->team->slack_notifications_scheduled_tasks = $this->slackNotificationsScheduledTasks;
+ $this->team->slack_notifications_server_disk_usage = $this->slackNotificationsServerDiskUsage;
+ $this->team->save();
+ refreshSession();
+ } else {
+ $this->slackEnabled = $this->team->slack_enabled;
+ $this->slackWebhookUrl = $this->team->slack_webhook_url;
+ $this->slackNotificationsTest = $this->team->slack_notifications_test;
+ $this->slackNotificationsDeployments = $this->team->slack_notifications_deployments;
+ $this->slackNotificationsStatusChanges = $this->team->slack_notifications_status_changes;
+ $this->slackNotificationsDatabaseBackups = $this->team->slack_notifications_database_backups;
+ $this->slackNotificationsScheduledTasks = $this->team->slack_notifications_scheduled_tasks;
+ $this->slackNotificationsServerDiskUsage = $this->team->slack_notifications_server_disk_usage;
+ }
+ }
+
+ public function instantSaveSlackEnabled()
+ {
+ try {
+ $this->validate([
+ 'slackWebhookUrl' => 'required',
+ ], [
+ 'slackWebhookUrl.required' => 'Slack Webhook URL is required.',
+ ]);
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ $this->slackEnabled = false;
+
+ return handleError($e, $this);
+ }
+ }
+
+ public function instantSave()
+ {
+ try {
+ $this->syncData(true);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function submit()
+ {
+ try {
+ $this->resetErrorBag();
+ $this->syncData(true);
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function saveModel()
+ {
+ $this->syncData(true);
+ refreshSession();
+ $this->dispatch('success', 'Settings saved.');
+ }
+
+ public function sendTestNotification()
+ {
+ try {
+ $this->team->notify(new Test);
+ $this->dispatch('success', 'Test notification sent.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function render()
+ {
+ return view('livewire.notifications.slack');
+ }
+}
diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php
index 862b9b3ea..15ec20577 100644
--- a/app/Livewire/Notifications/Telegram.php
+++ b/app/Livewire/Notifications/Telegram.php
@@ -4,67 +4,157 @@ namespace App\Livewire\Notifications;
use App\Models\Team;
use App\Notifications\Test;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Telegram extends Component
{
public Team $team;
- protected $rules = [
- 'team.telegram_enabled' => 'nullable|boolean',
- 'team.telegram_token' => 'required|string',
- 'team.telegram_chat_id' => 'required|string',
- 'team.telegram_notifications_test' => 'nullable|boolean',
- 'team.telegram_notifications_deployments' => 'nullable|boolean',
- 'team.telegram_notifications_status_changes' => 'nullable|boolean',
- 'team.telegram_notifications_database_backups' => 'nullable|boolean',
- 'team.telegram_notifications_scheduled_tasks' => 'nullable|boolean',
- 'team.telegram_notifications_test_message_thread_id' => 'nullable|string',
- 'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string',
- 'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string',
- 'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string',
- 'team.telegram_notifications_scheduled_tasks_thread_id' => 'nullable|string',
- 'team.telegram_notifications_server_disk_usage' => 'nullable|boolean',
- ];
+ #[Validate(['boolean'])]
+ public bool $telegramEnabled = false;
- protected $validationAttributes = [
- 'team.telegram_token' => 'Token',
- 'team.telegram_chat_id' => 'Chat ID',
- ];
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramToken = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramChatId = null;
+
+ #[Validate(['boolean'])]
+ public bool $telegramNotificationsTest = false;
+
+ #[Validate(['boolean'])]
+ public bool $telegramNotificationsDeployments = false;
+
+ #[Validate(['boolean'])]
+ public bool $telegramNotificationsStatusChanges = false;
+
+ #[Validate(['boolean'])]
+ public bool $telegramNotificationsDatabaseBackups = false;
+
+ #[Validate(['boolean'])]
+ public bool $telegramNotificationsScheduledTasks = false;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsTestMessageThreadId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsDeploymentsMessageThreadId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsStatusChangesMessageThreadId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsDatabaseBackupsMessageThreadId = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $telegramNotificationsScheduledTasksThreadId = null;
+
+ #[Validate(['boolean'])]
+ public bool $telegramNotificationsServerDiskUsage = false;
public function mount()
{
- $this->team = auth()->user()->currentTeam();
+ try {
+ $this->team = auth()->user()->currentTeam();
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->team->telegram_enabled = $this->telegramEnabled;
+ $this->team->telegram_token = $this->telegramToken;
+ $this->team->telegram_chat_id = $this->telegramChatId;
+ $this->team->telegram_notifications_test = $this->telegramNotificationsTest;
+ $this->team->telegram_notifications_deployments = $this->telegramNotificationsDeployments;
+ $this->team->telegram_notifications_status_changes = $this->telegramNotificationsStatusChanges;
+ $this->team->telegram_notifications_database_backups = $this->telegramNotificationsDatabaseBackups;
+ $this->team->telegram_notifications_scheduled_tasks = $this->telegramNotificationsScheduledTasks;
+ $this->team->telegram_notifications_test_message_thread_id = $this->telegramNotificationsTestMessageThreadId;
+ $this->team->telegram_notifications_deployments_message_thread_id = $this->telegramNotificationsDeploymentsMessageThreadId;
+ $this->team->telegram_notifications_status_changes_message_thread_id = $this->telegramNotificationsStatusChangesMessageThreadId;
+ $this->team->telegram_notifications_database_backups_message_thread_id = $this->telegramNotificationsDatabaseBackupsMessageThreadId;
+ $this->team->telegram_notifications_scheduled_tasks_thread_id = $this->telegramNotificationsScheduledTasksThreadId;
+ $this->team->telegram_notifications_server_disk_usage = $this->telegramNotificationsServerDiskUsage;
+ $this->team->save();
+ refreshSession();
+ } else {
+ $this->telegramEnabled = $this->team->telegram_enabled;
+ $this->telegramToken = $this->team->telegram_token;
+ $this->telegramChatId = $this->team->telegram_chat_id;
+ $this->telegramNotificationsTest = $this->team->telegram_notifications_test;
+ $this->telegramNotificationsDeployments = $this->team->telegram_notifications_deployments;
+ $this->telegramNotificationsStatusChanges = $this->team->telegram_notifications_status_changes;
+ $this->telegramNotificationsDatabaseBackups = $this->team->telegram_notifications_database_backups;
+ $this->telegramNotificationsScheduledTasks = $this->team->telegram_notifications_scheduled_tasks;
+ $this->telegramNotificationsTestMessageThreadId = $this->team->telegram_notifications_test_message_thread_id;
+ $this->telegramNotificationsDeploymentsMessageThreadId = $this->team->telegram_notifications_deployments_message_thread_id;
+ $this->telegramNotificationsStatusChangesMessageThreadId = $this->team->telegram_notifications_status_changes_message_thread_id;
+ $this->telegramNotificationsDatabaseBackupsMessageThreadId = $this->team->telegram_notifications_database_backups_message_thread_id;
+ $this->telegramNotificationsScheduledTasksThreadId = $this->team->telegram_notifications_scheduled_tasks_thread_id;
+ $this->telegramNotificationsServerDiskUsage = $this->team->telegram_notifications_server_disk_usage;
+ }
+
}
public function instantSave()
{
try {
- $this->submit();
- } catch (\Throwable) {
- $this->team->telegram_enabled = false;
- $this->validate();
+ $this->syncData(true);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
public function submit()
{
- $this->resetErrorBag();
- $this->validate();
- $this->saveModel();
+ try {
+ $this->resetErrorBag();
+ $this->syncData(true);
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function instantSaveTelegramEnabled()
+ {
+ try {
+ $this->validate([
+ 'telegramToken' => 'required',
+ 'telegramChatId' => 'required',
+ ], [
+ 'telegramToken.required' => 'Telegram Token is required.',
+ 'telegramChatId.required' => 'Telegram Chat ID is required.',
+ ]);
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ $this->telegramEnabled = false;
+
+ return handleError($e, $this);
+ }
}
public function saveModel()
{
- $this->team->save();
+ $this->syncData(true);
refreshSession();
$this->dispatch('success', 'Settings saved.');
}
public function sendTestNotification()
{
- $this->team?->notify(new Test);
- $this->dispatch('success', 'Test notification sent.');
+ try {
+ $this->team->notify(new Test);
+ $this->dispatch('success', 'Test notification sent.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function render()
diff --git a/app/Livewire/Profile/Index.php b/app/Livewire/Profile/Index.php
index 483f68cf8..53314cd5c 100644
--- a/app/Livewire/Profile/Index.php
+++ b/app/Livewire/Profile/Index.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Profile;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
use Livewire\Attributes\Validate;
@@ -24,9 +25,9 @@ class Index extends Component
public function mount()
{
- $this->userId = auth()->user()->id;
- $this->name = auth()->user()->name;
- $this->email = auth()->user()->email;
+ $this->userId = Auth::id();
+ $this->name = Auth::user()->name;
+ $this->email = Auth::user()->email;
}
public function submit()
@@ -35,7 +36,7 @@ class Index extends Component
$this->validate([
'name' => 'required',
]);
- auth()->user()->update([
+ Auth::user()->update([
'name' => $this->name,
]);
diff --git a/app/Livewire/Project/AddEmpty.php b/app/Livewire/Project/AddEmpty.php
index c8c063960..fd976548a 100644
--- a/app/Livewire/Project/AddEmpty.php
+++ b/app/Livewire/Project/AddEmpty.php
@@ -3,15 +3,15 @@
namespace App\Livewire\Project;
use App\Models\Project;
-use Livewire\Attributes\Rule;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class AddEmpty extends Component
{
- #[Rule(['required', 'string', 'min:3'])]
+ #[Validate(['required', 'string', 'min:3'])]
public string $name;
- #[Rule(['nullable', 'string'])]
+ #[Validate(['nullable', 'string'])]
public string $description = '';
public function submit()
diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php
index a3a688f7c..cb63f0e1a 100644
--- a/app/Livewire/Project/Application/Advanced.php
+++ b/app/Livewire/Project/Application/Advanced.php
@@ -3,120 +3,205 @@
namespace App\Livewire\Project\Application;
use App\Models\Application;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Advanced extends Component
{
public Application $application;
- public bool $is_force_https_enabled;
+ #[Validate(['boolean'])]
+ public bool $isForceHttpsEnabled = false;
- public bool $is_gzip_enabled;
+ #[Validate(['boolean'])]
+ public bool $isGitSubmodulesEnabled = false;
- public bool $is_stripprefix_enabled;
+ #[Validate(['boolean'])]
+ public bool $isGitLfsEnabled = false;
- protected $rules = [
- 'application.settings.is_git_submodules_enabled' => 'boolean|required',
- 'application.settings.is_git_lfs_enabled' => 'boolean|required',
- 'application.settings.is_preview_deployments_enabled' => 'boolean|required',
- 'application.settings.is_auto_deploy_enabled' => 'boolean|required',
- 'is_force_https_enabled' => 'boolean|required',
- 'application.settings.is_log_drain_enabled' => 'boolean|required',
- 'application.settings.is_gpu_enabled' => 'boolean|required',
- 'application.settings.is_build_server_enabled' => 'boolean|required',
- 'application.settings.is_consistent_container_name_enabled' => 'boolean|required',
- 'application.settings.custom_internal_name' => 'string|nullable',
- 'application.settings.is_gzip_enabled' => 'boolean|required',
- 'application.settings.is_stripprefix_enabled' => 'boolean|required',
- 'application.settings.gpu_driver' => 'string|required',
- 'application.settings.gpu_count' => 'string|required',
- 'application.settings.gpu_device_ids' => 'string|required',
- 'application.settings.gpu_options' => 'string|required',
- 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required',
- 'application.settings.connect_to_docker_network' => 'boolean|required',
- ];
+ #[Validate(['boolean'])]
+ public bool $isPreviewDeploymentsEnabled = false;
+
+ #[Validate(['boolean'])]
+ public bool $isAutoDeployEnabled = true;
+
+ #[Validate(['boolean'])]
+ public bool $disableBuildCache = false;
+
+ #[Validate(['boolean'])]
+ public bool $isLogDrainEnabled = false;
+
+ #[Validate(['boolean'])]
+ public bool $isGpuEnabled = false;
+
+ #[Validate(['string'])]
+ public string $gpuDriver = '';
+
+ #[Validate(['string', 'nullable'])]
+ public ?string $gpuCount = null;
+
+ #[Validate(['string', 'nullable'])]
+ public ?string $gpuDeviceIds = null;
+
+ #[Validate(['string', 'nullable'])]
+ public ?string $gpuOptions = null;
+
+ #[Validate(['boolean'])]
+ public bool $isBuildServerEnabled = false;
+
+ #[Validate(['boolean'])]
+ public bool $isConsistentContainerNameEnabled = false;
+
+ #[Validate(['string', 'nullable'])]
+ public ?string $customInternalName = null;
+
+ #[Validate(['boolean'])]
+ public bool $isGzipEnabled = true;
+
+ #[Validate(['boolean'])]
+ public bool $isStripprefixEnabled = true;
+
+ #[Validate(['boolean'])]
+ public bool $isRawComposeDeploymentEnabled = false;
+
+ #[Validate(['boolean'])]
+ public bool $isConnectToDockerNetworkEnabled = false;
public function mount()
{
- $this->is_force_https_enabled = $this->application->isForceHttpsEnabled();
- $this->is_gzip_enabled = $this->application->isGzipEnabled();
- $this->is_stripprefix_enabled = $this->application->isStripprefixEnabled();
+ try {
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->application->settings->is_force_https_enabled = $this->isForceHttpsEnabled;
+ $this->application->settings->is_git_submodules_enabled = $this->isGitSubmodulesEnabled;
+ $this->application->settings->is_git_lfs_enabled = $this->isGitLfsEnabled;
+ $this->application->settings->is_preview_deployments_enabled = $this->isPreviewDeploymentsEnabled;
+ $this->application->settings->is_auto_deploy_enabled = $this->isAutoDeployEnabled;
+ $this->application->settings->is_log_drain_enabled = $this->isLogDrainEnabled;
+ $this->application->settings->is_gpu_enabled = $this->isGpuEnabled;
+ $this->application->settings->gpu_driver = $this->gpuDriver;
+ $this->application->settings->gpu_count = $this->gpuCount;
+ $this->application->settings->gpu_device_ids = $this->gpuDeviceIds;
+ $this->application->settings->gpu_options = $this->gpuOptions;
+ $this->application->settings->is_build_server_enabled = $this->isBuildServerEnabled;
+ $this->application->settings->is_consistent_container_name_enabled = $this->isConsistentContainerNameEnabled;
+ $this->application->settings->custom_internal_name = $this->customInternalName;
+ $this->application->settings->is_gzip_enabled = $this->isGzipEnabled;
+ $this->application->settings->is_stripprefix_enabled = $this->isStripprefixEnabled;
+ $this->application->settings->is_raw_compose_deployment_enabled = $this->isRawComposeDeploymentEnabled;
+ $this->application->settings->connect_to_docker_network = $this->isConnectToDockerNetworkEnabled;
+ $this->application->settings->disable_build_cache = $this->disableBuildCache;
+ $this->application->settings->save();
+ } else {
+ $this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled();
+ $this->isGzipEnabled = $this->application->isGzipEnabled();
+ $this->isStripprefixEnabled = $this->application->isStripprefixEnabled();
+ $this->isLogDrainEnabled = $this->application->isLogDrainEnabled();
+
+ $this->isGitSubmodulesEnabled = $this->application->settings->is_git_submodules_enabled;
+ $this->isGitLfsEnabled = $this->application->settings->is_git_lfs_enabled;
+ $this->isPreviewDeploymentsEnabled = $this->application->settings->is_preview_deployments_enabled;
+ $this->isAutoDeployEnabled = $this->application->settings->is_auto_deploy_enabled;
+ $this->isGpuEnabled = $this->application->settings->is_gpu_enabled;
+ $this->gpuDriver = $this->application->settings->gpu_driver;
+ $this->gpuCount = $this->application->settings->gpu_count;
+ $this->gpuDeviceIds = $this->application->settings->gpu_device_ids;
+ $this->gpuOptions = $this->application->settings->gpu_options;
+ $this->isBuildServerEnabled = $this->application->settings->is_build_server_enabled;
+ $this->isConsistentContainerNameEnabled = $this->application->settings->is_consistent_container_name_enabled;
+ $this->customInternalName = $this->application->settings->custom_internal_name;
+ $this->isRawComposeDeploymentEnabled = $this->application->settings->is_raw_compose_deployment_enabled;
+ $this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network;
+ $this->disableBuildCache = $this->application->settings->disable_build_cache;
+ }
}
public function instantSave()
{
- if ($this->application->isLogDrainEnabled()) {
- if (! $this->application->destination->server->isLogDrainEnabled()) {
- $this->application->settings->is_log_drain_enabled = false;
- $this->dispatch('error', 'Log drain is not enabled on this server.');
+ try {
+ if ($this->isLogDrainEnabled) {
+ if (! $this->application->destination->server->isLogDrainEnabled()) {
+ $this->isLogDrainEnabled = false;
+ $this->syncData(true);
+ $this->dispatch('error', 'Log drain is not enabled on this server.');
- return;
+ return;
+ }
}
+ if ($this->application->isForceHttpsEnabled() !== $this->isForceHttpsEnabled ||
+ $this->application->isGzipEnabled() !== $this->isGzipEnabled ||
+ $this->application->isStripprefixEnabled() !== $this->isStripprefixEnabled
+ ) {
+ $this->dispatch('resetDefaultLabels', false);
+ }
+
+ if ($this->application->settings->is_raw_compose_deployment_enabled) {
+ $this->application->oldRawParser();
+ } else {
+ $this->application->parse();
+ }
+ $this->syncData(true);
+ $this->dispatch('success', 'Settings saved.');
+ $this->dispatch('configurationChanged');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- if ($this->application->settings->is_force_https_enabled !== $this->is_force_https_enabled) {
- $this->application->settings->is_force_https_enabled = $this->is_force_https_enabled;
- $this->dispatch('resetDefaultLabels', false);
- }
- if ($this->application->settings->is_gzip_enabled !== $this->is_gzip_enabled) {
- $this->application->settings->is_gzip_enabled = $this->is_gzip_enabled;
- $this->dispatch('resetDefaultLabels', false);
- }
- if ($this->application->settings->is_stripprefix_enabled !== $this->is_stripprefix_enabled) {
- $this->application->settings->is_stripprefix_enabled = $this->is_stripprefix_enabled;
- $this->dispatch('resetDefaultLabels', false);
- }
- if ($this->application->settings->is_raw_compose_deployment_enabled) {
- $this->application->oldRawParser();
- } else {
- $this->application->parse();
- }
- $this->application->settings->save();
- $this->dispatch('success', 'Settings saved.');
- $this->dispatch('configurationChanged');
}
public function submit()
{
- if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) {
- $this->dispatch('error', 'You cannot set both GPU count and GPU device IDs.');
- $this->application->settings->gpu_count = null;
- $this->application->settings->gpu_device_ids = null;
- $this->application->settings->save();
+ try {
+ if ($this->gpuCount && $this->gpuDeviceIds) {
+ $this->dispatch('error', 'You cannot set both GPU count and GPU device IDs.');
+ $this->gpuCount = null;
+ $this->gpuDeviceIds = null;
+ $this->syncData(true);
- return;
+ return;
+ }
+ $this->syncData(true);
+ $this->dispatch('success', 'Settings saved.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- $this->application->settings->save();
- $this->dispatch('success', 'Settings saved.');
}
public function saveCustomName()
{
- if (str($this->application->settings->custom_internal_name)->isNotEmpty()) {
- $this->application->settings->custom_internal_name = str($this->application->settings->custom_internal_name)->slug()->value();
+ if (str($this->customInternalName)->isNotEmpty()) {
+ $this->customInternalName = str($this->customInternalName)->slug()->value();
} else {
- $this->application->settings->custom_internal_name = null;
+ $this->customInternalName = null;
}
- if (is_null($this->application->settings->custom_internal_name)) {
- $this->application->settings->save();
+ if (is_null($this->customInternalName)) {
+ $this->syncData(true);
$this->dispatch('success', 'Custom name saved.');
return;
}
- $customInternalName = $this->application->settings->custom_internal_name;
+ $customInternalName = $this->customInternalName;
$server = $this->application->destination->server;
$allApplications = $server->applications();
$foundSameInternalName = $allApplications->filter(function ($application) {
- return $application->id !== $this->application->id && $application->settings->custom_internal_name === $this->application->settings->custom_internal_name;
+ return $application->id !== $this->application->id && $application->settings->custom_internal_name === $this->customInternalName;
});
if ($foundSameInternalName->isNotEmpty()) {
$this->dispatch('error', 'This custom container name is already in use by another application on this server.');
- $this->application->settings->custom_internal_name = $customInternalName;
- $this->application->settings->refresh();
+ $this->customInternalName = $customInternalName;
+ $this->syncData(true);
return;
}
- $this->application->settings->save();
+ $this->syncData(true);
$this->dispatch('success', 'Custom name saved.');
}
diff --git a/app/Livewire/Project/Application/Configuration.php b/app/Livewire/Project/Application/Configuration.php
index d4ec8f581..5261a0800 100644
--- a/app/Livewire/Project/Application/Configuration.php
+++ b/app/Livewire/Project/Application/Configuration.php
@@ -16,24 +16,30 @@ class Configuration extends Component
public function mount()
{
- $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
- if (! $project) {
- return redirect()->route('dashboard');
- }
- $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
- if (! $environment) {
- return redirect()->route('dashboard');
- }
- $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
- if (! $application) {
- return redirect()->route('dashboard');
- }
+ $project = currentTeam()
+ ->projects()
+ ->select('id', 'uuid', 'team_id')
+ ->where('uuid', request()->route('project_uuid'))
+ ->firstOrFail();
+ $environment = $project->environments()
+ ->select('id', 'name', 'project_id')
+ ->where('name', request()->route('environment_name'))
+ ->firstOrFail();
+ $application = $environment->applications()
+ ->with(['destination'])
+ ->where('uuid', request()->route('application_uuid'))
+ ->firstOrFail();
+
$this->application = $application;
- $mainServer = $this->application->destination->server;
- $servers = Server::ownedByCurrentTeam()->get();
- $this->servers = $servers->filter(function ($server) use ($mainServer) {
- return $server->id != $mainServer->id;
- });
+ if ($application->destination && $application->destination->server) {
+ $mainServer = $application->destination->server;
+ $this->servers = Server::ownedByCurrentTeam()
+ ->select('id', 'name')
+ ->where('id', '!=', $mainServer->id)
+ ->get();
+ } else {
+ $this->servers = collect();
+ }
}
public function render()
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index f1575a01f..ff29b74e9 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -84,6 +84,7 @@ class General extends Component
'application.pre_deployment_command_container' => 'nullable',
'application.post_deployment_command' => 'nullable',
'application.post_deployment_command_container' => 'nullable',
+ 'application.custom_nginx_configuration' => 'nullable',
'application.settings.is_static' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.is_container_label_escape_enabled' => 'boolean|required',
@@ -121,6 +122,7 @@ class General extends Component
'application.custom_docker_run_options' => 'Custom docker run commands',
'application.docker_compose_custom_start_command' => 'Docker compose custom start command',
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
+ 'application.custom_nginx_configuration' => 'Custom Nginx configuration',
'application.settings.is_static' => 'Is static',
'application.settings.is_build_server_enabled' => 'Is build server enabled',
'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled',
@@ -241,6 +243,13 @@ class General extends Component
}
}
+ public function updatedApplicationSettingsIsStatic($value)
+ {
+ if ($value) {
+ $this->generateNginxConfiguration();
+ }
+ }
+
public function updatedApplicationBuildPack()
{
if ($this->application->build_pack !== 'nixpacks') {
@@ -257,6 +266,7 @@ class General extends Component
if ($this->application->build_pack === 'static') {
$this->application->ports_exposes = $this->ports_exposes = 80;
$this->resetDefaultLabels(false);
+ $this->generateNginxConfiguration();
}
$this->submit();
$this->dispatch('buildPackUpdated');
@@ -274,6 +284,13 @@ class General extends Component
}
}
+ public function generateNginxConfiguration()
+ {
+ $this->application->custom_nginx_configuration = defaultNginxConfiguration();
+ $this->application->save();
+ $this->dispatch('success', 'Nginx configuration generated.');
+ }
+
public function resetDefaultLabels($manualReset = false)
{
try {
diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php
index 1082b48cd..19a6145b7 100644
--- a/app/Livewire/Project/Application/Heading.php
+++ b/app/Livewire/Project/Application/Heading.php
@@ -36,7 +36,11 @@ class Heading extends Component
public function mount()
{
- $this->parameters = get_route_parameters();
+ $this->parameters = [
+ 'project_uuid' => $this->application->project()->uuid,
+ 'environment_name' => $this->application->environment->name,
+ 'application_uuid' => $this->application->uuid,
+ ];
$lastDeployment = $this->application->get_last_successful_deployment();
$this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7).' '.data_get($lastDeployment, 'commit_message');
$this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit'));
@@ -45,13 +49,11 @@ class Heading extends Component
public function check_status($showNotification = false)
{
if ($this->application->destination->server->isFunctional()) {
- GetContainersStatus::dispatch($this->application->destination->server)->onQueue('high');
+ GetContainersStatus::dispatch($this->application->destination->server);
}
if ($showNotification) {
$this->dispatch('success', 'Success', 'Application status updated.');
}
- // Removed because it caused flickering
- // $this->dispatch('configurationChanged');
}
public function force_deploy_without_cache()
diff --git a/app/Livewire/Project/Application/Preview/Form.php b/app/Livewire/Project/Application/Preview/Form.php
index 73b423f90..edcab44c8 100644
--- a/app/Livewire/Project/Application/Preview/Form.php
+++ b/app/Livewire/Project/Application/Preview/Form.php
@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Application\Preview;
use App\Models\Application;
+use Livewire\Attributes\Validate;
use Livewire\Component;
use Spatie\Url\Url;
@@ -10,49 +11,53 @@ class Form extends Component
{
public Application $application;
- public string $preview_url_template;
-
- protected $rules = [
- 'application.preview_url_template' => 'required',
- ];
-
- protected $validationAttributes = [
- 'application.preview_url_template' => 'preview url template',
- ];
-
- public function resetToDefault()
- {
- $this->application->preview_url_template = '{{pr_id}}.{{domain}}';
- $this->preview_url_template = $this->application->preview_url_template;
- $this->application->save();
- $this->generate_real_url();
- }
-
- public function generate_real_url()
- {
- if (data_get($this->application, 'fqdn')) {
- try {
- $firstFqdn = str($this->application->fqdn)->before(',');
- $url = Url::fromString($firstFqdn);
- $host = $url->getHost();
- $this->preview_url_template = str($this->application->preview_url_template)->replace('{{domain}}', $host);
- } catch (\Exception) {
- $this->dispatch('error', 'Invalid FQDN.');
- }
- }
- }
+ #[Validate('required')]
+ public string $previewUrlTemplate;
public function mount()
{
- $this->generate_real_url();
+ try {
+ $this->previewUrlTemplate = $this->application->preview_url_template;
+ $this->generateRealUrl();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function submit()
{
- $this->validate();
- $this->application->preview_url_template = str_replace(' ', '', $this->application->preview_url_template);
- $this->application->save();
- $this->dispatch('success', 'Preview url template updated.');
- $this->generate_real_url();
+ try {
+ $this->resetErrorBag();
+ $this->validate();
+ $this->application->preview_url_template = str_replace(' ', '', $this->previewUrlTemplate);
+ $this->application->save();
+ $this->dispatch('success', 'Preview url template updated.');
+ $this->generateRealUrl();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function resetToDefault()
+ {
+ try {
+ $this->application->preview_url_template = '{{pr_id}}.{{domain}}';
+ $this->previewUrlTemplate = $this->application->preview_url_template;
+ $this->application->save();
+ $this->generateRealUrl();
+ $this->dispatch('success', 'Preview url template updated.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function generateRealUrl()
+ {
+ if (data_get($this->application, 'fqdn')) {
+ $firstFqdn = str($this->application->fqdn)->before(',');
+ $url = Url::fromString($firstFqdn);
+ $host = $url->getHost();
+ $this->previewUrlTemplate = str($this->application->preview_url_template)->replace('{{domain}}', $host);
+ }
}
}
diff --git a/app/Livewire/Project/Application/Source.php b/app/Livewire/Project/Application/Source.php
index 2d75d91f2..ade297d50 100644
--- a/app/Livewire/Project/Application/Source.php
+++ b/app/Livewire/Project/Application/Source.php
@@ -5,7 +5,7 @@ namespace App\Livewire\Project\Application;
use App\Models\Application;
use App\Models\PrivateKey;
use Livewire\Attributes\Locked;
-use Livewire\Attributes\Rule;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Source extends Component
@@ -15,19 +15,19 @@ class Source extends Component
#[Locked]
public $privateKeys;
- #[Rule(['nullable', 'string'])]
+ #[Validate(['nullable', 'string'])]
public ?string $privateKeyName = null;
- #[Rule(['nullable', 'integer'])]
+ #[Validate(['nullable', 'integer'])]
public ?int $privateKeyId = null;
- #[Rule(['required', 'string'])]
+ #[Validate(['required', 'string'])]
public string $gitRepository;
- #[Rule(['required', 'string'])]
+ #[Validate(['required', 'string'])]
public string $gitBranch;
- #[Rule(['nullable', 'string'])]
+ #[Validate(['nullable', 'string'])]
public ?string $gitCommitSha = null;
public function mount()
diff --git a/app/Livewire/Project/Application/Swarm.php b/app/Livewire/Project/Application/Swarm.php
index 0151b5222..197dc41ed 100644
--- a/app/Livewire/Project/Application/Swarm.php
+++ b/app/Livewire/Project/Application/Swarm.php
@@ -3,32 +3,55 @@
namespace App\Livewire\Project\Application;
use App\Models\Application;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Swarm extends Component
{
public Application $application;
- public string $swarm_placement_constraints = '';
+ #[Validate('required')]
+ public int $swarmReplicas;
- protected $rules = [
- 'application.swarm_replicas' => 'required',
- 'application.swarm_placement_constraints' => 'nullable',
- 'application.settings.is_swarm_only_worker_nodes' => 'required',
- ];
+ #[Validate(['nullable'])]
+ public ?string $swarmPlacementConstraints = null;
+
+ #[Validate('required')]
+ public bool $isSwarmOnlyWorkerNodes;
public function mount()
{
- if ($this->application->swarm_placement_constraints) {
- $this->swarm_placement_constraints = base64_decode($this->application->swarm_placement_constraints);
+ try {
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->application->swarm_replicas = $this->swarmReplicas;
+ $this->application->swarm_placement_constraints = $this->swarmPlacementConstraints ? base64_encode($this->swarmPlacementConstraints) : null;
+ $this->application->settings->is_swarm_only_worker_nodes = $this->isSwarmOnlyWorkerNodes;
+ $this->application->save();
+ $this->application->settings->save();
+ } else {
+ $this->swarmReplicas = $this->application->swarm_replicas;
+ if ($this->application->swarm_placement_constraints) {
+ $this->swarmPlacementConstraints = base64_decode($this->application->swarm_placement_constraints);
+ } else {
+ $this->swarmPlacementConstraints = null;
+ }
+ $this->isSwarmOnlyWorkerNodes = $this->application->settings->is_swarm_only_worker_nodes;
}
}
public function instantSave()
{
try {
- $this->validate();
- $this->application->settings->save();
+ $this->syncData(true);
$this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -38,14 +61,7 @@ class Swarm extends Component
public function submit()
{
try {
- $this->validate();
- if ($this->swarm_placement_constraints) {
- $this->application->swarm_placement_constraints = base64_encode($this->swarm_placement_constraints);
- } else {
- $this->application->swarm_placement_constraints = null;
- }
- $this->application->save();
-
+ $this->syncData(true);
$this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php
index 9b82c4b11..b3a54f0ab 100644
--- a/app/Livewire/Project/Database/BackupEdit.php
+++ b/app/Livewire/Project/Database/BackupEdit.php
@@ -4,56 +4,87 @@ namespace App\Livewire\Project\Database;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
+use Exception;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
use Spatie\Url\Url;
class BackupEdit extends Component
{
- public ?ScheduledDatabaseBackup $backup;
+ public ScheduledDatabaseBackup $backup;
+ #[Locked]
public $s3s;
+ #[Locked]
+ public $parameters;
+
+ #[Validate(['required', 'boolean'])]
public bool $delete_associated_backups_locally = false;
+ #[Validate(['required', 'boolean'])]
public bool $delete_associated_backups_s3 = false;
+ #[Validate(['required', 'boolean'])]
public bool $delete_associated_backups_sftp = false;
+ #[Validate(['nullable', 'string'])]
public ?string $status = null;
- public array $parameters;
+ #[Validate(['required', 'boolean'])]
+ public bool $backupEnabled = false;
- protected $rules = [
- 'backup.enabled' => 'required|boolean',
- 'backup.frequency' => 'required|string',
- 'backup.number_of_backups_locally' => 'required|integer|min:1',
- 'backup.save_s3' => 'required|boolean',
- 'backup.s3_storage_id' => 'nullable|integer',
- 'backup.databases_to_backup' => 'nullable',
- 'backup.dump_all' => 'required|boolean',
- ];
+ #[Validate(['required', 'string'])]
+ public string $frequency = '';
- protected $validationAttributes = [
- 'backup.enabled' => 'Enabled',
- 'backup.frequency' => 'Frequency',
- 'backup.number_of_backups_locally' => 'Number of Backups Locally',
- 'backup.save_s3' => 'Save to S3',
- 'backup.s3_storage_id' => 'S3 Storage',
- 'backup.databases_to_backup' => 'Databases to Backup',
- 'backup.dump_all' => 'Backup All Databases',
- ];
+ #[Validate(['required', 'integer', 'min:1'])]
+ public int $numberOfBackupsLocally = 1;
- protected $messages = [
- 'backup.s3_storage_id' => 'Select a S3 Storage',
- ];
+ #[Validate(['required', 'boolean'])]
+ public bool $saveS3 = false;
+
+ #[Validate(['nullable', 'integer'])]
+ public ?int $s3StorageId = 1;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $databasesToBackup = null;
+
+ #[Validate(['required', 'boolean'])]
+ public bool $dumpAll = false;
public function mount()
{
- $this->parameters = get_route_parameters();
- if (is_null(data_get($this->backup, 's3_storage_id'))) {
- data_set($this->backup, 's3_storage_id', 'default');
+ try {
+ $this->parameters = get_route_parameters();
+ $this->syncData();
+ } catch (Exception $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->customValidate();
+ $this->backup->enabled = $this->backupEnabled;
+ $this->backup->frequency = $this->frequency;
+ $this->backup->number_of_backups_locally = $this->numberOfBackupsLocally;
+ $this->backup->save_s3 = $this->saveS3;
+ $this->backup->s3_storage_id = $this->s3StorageId;
+ $this->backup->databases_to_backup = $this->databasesToBackup;
+ $this->backup->dump_all = $this->dumpAll;
+ $this->backup->save();
+ } else {
+ $this->backupEnabled = $this->backup->enabled;
+ $this->frequency = $this->backup->frequency;
+ $this->numberOfBackupsLocally = $this->backup->number_of_backups_locally;
+ $this->saveS3 = $this->backup->save_s3;
+ $this->s3StorageId = $this->backup->s3_storage_id;
+ $this->databasesToBackup = $this->backup->databases_to_backup;
+ $this->dumpAll = $this->backup->dump_all;
}
}
@@ -96,16 +127,14 @@ class BackupEdit extends Component
public function instantSave()
{
try {
- $this->custom_validate();
- $this->backup->save();
- $this->backup->refresh();
+ $this->syncData(true);
$this->dispatch('success', 'Backup updated successfully.');
} catch (\Throwable $e) {
$this->dispatch('error', $e->getMessage());
}
}
- private function custom_validate()
+ private function customValidate()
{
if (! is_numeric($this->backup->s3_storage_id)) {
$this->backup->s3_storage_id = null;
@@ -120,19 +149,14 @@ class BackupEdit extends Component
public function submit()
{
try {
- $this->custom_validate();
- if ($this->backup->databases_to_backup === '' || $this->backup->databases_to_backup === null) {
- $this->backup->databases_to_backup = null;
- }
- $this->backup->save();
- $this->backup->refresh();
- $this->dispatch('success', 'Backup updated successfully');
+ $this->syncData(true);
+ $this->dispatch('success', 'Backup updated successfully.');
} catch (\Throwable $e) {
$this->dispatch('error', $e->getMessage());
}
}
- public function deleteAssociatedBackupsLocally()
+ private function deleteAssociatedBackupsLocally()
{
$executions = $this->backup->executions;
$backupFolder = null;
@@ -152,17 +176,17 @@ class BackupEdit extends Component
$execution->delete();
}
- if ($backupFolder) {
+ if (str($backupFolder)->isNotEmpty()) {
$this->deleteEmptyBackupFolder($backupFolder, $server);
}
}
- public function deleteAssociatedBackupsS3()
+ private function deleteAssociatedBackupsS3()
{
//Add function to delete backups from S3
}
- public function deleteAssociatedBackupsSftp()
+ private function deleteAssociatedBackupsSftp()
{
//Add function to delete backups from SFTP
}
diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php
index 7a6446815..2d39c5151 100644
--- a/app/Livewire/Project/Database/Clickhouse/General.php
+++ b/app/Livewire/Project/Database/Clickhouse/General.php
@@ -7,6 +7,8 @@ use App\Actions\Database\StopDatabaseProxy;
use App\Models\Server;
use App\Models\StandaloneClickhouse;
use Exception;
+use Illuminate\Support\Facades\Auth;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class General extends Component
@@ -15,54 +17,106 @@ class General extends Component
public StandaloneClickhouse $database;
- public ?string $db_url = null;
+ #[Validate(['required', 'string'])]
+ public string $name;
- public ?string $db_url_public = null;
+ #[Validate(['nullable', 'string'])]
+ public ?string $description = null;
- protected $listeners = ['refresh'];
+ #[Validate(['required', 'string'])]
+ public string $clickhouseAdminUser;
- protected $rules = [
- 'database.name' => 'required',
- 'database.description' => 'nullable',
- 'database.clickhouse_admin_user' => 'required',
- 'database.clickhouse_admin_password' => 'required',
- 'database.image' => 'required',
- 'database.ports_mappings' => 'nullable',
- 'database.is_public' => 'nullable|boolean',
- 'database.public_port' => 'nullable|integer',
- 'database.is_log_drain_enabled' => 'nullable|boolean',
- 'database.custom_docker_run_options' => 'nullable',
- ];
+ #[Validate(['required', 'string'])]
+ public string $clickhouseAdminPassword;
- protected $validationAttributes = [
- 'database.name' => 'Name',
- 'database.description' => 'Description',
- 'database.clickhouse_admin_user' => 'Postgres User',
- 'database.clickhouse_admin_password' => 'Postgres Password',
- 'database.image' => 'Image',
- 'database.ports_mappings' => 'Port Mapping',
- 'database.is_public' => 'Is Public',
- 'database.public_port' => 'Public Port',
- 'database.custom_docker_run_options' => 'Custom Docker Run Options',
- ];
+ #[Validate(['required', 'string'])]
+ public string $image;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $portsMappings = null;
+
+ #[Validate(['nullable', 'boolean'])]
+ public ?bool $isPublic = null;
+
+ #[Validate(['nullable', 'integer'])]
+ public ?int $publicPort = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $customDockerRunOptions = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $dbUrl = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $dbUrlPublic = null;
+
+ #[Validate(['nullable', 'boolean'])]
+ public bool $isLogDrainEnabled = false;
+
+ public function getListeners()
+ {
+ $teamId = Auth::user()->currentTeam()->id;
+
+ return [
+ "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
+ ];
+ }
public function mount()
{
- $this->db_url = $this->database->internal_db_url;
- $this->db_url_public = $this->database->external_db_url;
- $this->server = data_get($this->database, 'destination.server');
+ try {
+ $this->syncData();
+ $this->server = data_get($this->database, 'destination.server');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->database->name = $this->name;
+ $this->database->description = $this->description;
+ $this->database->clickhouse_admin_user = $this->clickhouseAdminUser;
+ $this->database->clickhouse_admin_password = $this->clickhouseAdminPassword;
+ $this->database->image = $this->image;
+ $this->database->ports_mappings = $this->portsMappings;
+ $this->database->is_public = $this->isPublic;
+ $this->database->public_port = $this->publicPort;
+ $this->database->custom_docker_run_options = $this->customDockerRunOptions;
+ $this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
+ $this->database->save();
+
+ $this->dbUrl = $this->database->internal_db_url;
+ $this->dbUrlPublic = $this->database->external_db_url;
+ } else {
+ $this->name = $this->database->name;
+ $this->description = $this->database->description;
+ $this->clickhouseAdminUser = $this->database->clickhouse_admin_user;
+ $this->clickhouseAdminPassword = $this->database->clickhouse_admin_password;
+ $this->image = $this->database->image;
+ $this->portsMappings = $this->database->ports_mappings;
+ $this->isPublic = $this->database->is_public;
+ $this->publicPort = $this->database->public_port;
+ $this->customDockerRunOptions = $this->database->custom_docker_run_options;
+ $this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
+ $this->dbUrl = $this->database->internal_db_url;
+ $this->dbUrlPublic = $this->database->external_db_url;
+ }
}
public function instantSaveAdvanced()
{
try {
if (! $this->server->isLogDrainEnabled()) {
- $this->database->is_log_drain_enabled = false;
+ $this->isLogDrainEnabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
- $this->database->save();
+ $this->syncData(true);
+
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
@@ -73,16 +127,16 @@ class General extends Component
public function instantSave()
{
try {
- if ($this->database->is_public && ! $this->database->public_port) {
+ if ($this->isPublic && ! $this->publicPort) {
$this->dispatch('error', 'Public port is required.');
- $this->database->is_public = false;
+ $this->isPublic = false;
return;
}
- if ($this->database->is_public) {
+ if ($this->isPublic) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
- $this->database->is_public = false;
+ $this->isPublic = false;
return;
}
@@ -92,28 +146,28 @@ class General extends Component
StopDatabaseProxy::run($this->database);
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
- $this->db_url_public = $this->database->external_db_url;
- $this->database->save();
+ $this->dbUrlPublic = $this->database->external_db_url;
+ $this->syncData(true);
} catch (\Throwable $e) {
- $this->database->is_public = ! $this->database->is_public;
+ $this->isPublic = ! $this->isPublic;
+ $this->syncData(true);
return handleError($e, $this);
}
}
- public function refresh(): void
+ public function databaseProxyStopped()
{
- $this->database->refresh();
+ $this->syncData();
}
public function submit()
{
try {
- if (str($this->database->public_port)->isEmpty()) {
- $this->database->public_port = null;
+ if (str($this->publicPort)->isEmpty()) {
+ $this->publicPort = null;
}
- $this->validate();
- $this->database->save();
+ $this->syncData(true);
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
diff --git a/app/Livewire/Project/Database/CreateScheduledBackup.php b/app/Livewire/Project/Database/CreateScheduledBackup.php
index 52bced44f..01108c290 100644
--- a/app/Livewire/Project/Database/CreateScheduledBackup.php
+++ b/app/Livewire/Project/Database/CreateScheduledBackup.php
@@ -4,59 +4,62 @@ namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use Illuminate\Support\Collection;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class CreateScheduledBackup extends Component
{
- public $database;
-
+ #[Validate(['required', 'string'])]
public $frequency;
+ #[Validate(['required', 'boolean'])]
+ public bool $saveToS3 = false;
+
+ #[Locked]
+ public $database;
+
public bool $enabled = true;
- public bool $save_s3 = false;
+ #[Validate(['nullable', 'integer'])]
+ public ?int $s3StorageId = null;
- public $s3_storage_id;
-
- public Collection $s3s;
-
- protected $rules = [
- 'frequency' => 'required|string',
- 'save_s3' => 'required|boolean',
- ];
-
- protected $validationAttributes = [
- 'frequency' => 'Backup Frequency',
- 'save_s3' => 'Save to S3',
- ];
+ public Collection $definedS3s;
public function mount()
{
- $this->s3s = currentTeam()->s3s;
- if ($this->s3s->count() > 0) {
- $this->s3_storage_id = $this->s3s->first()->id;
+ try {
+ $this->definedS3s = currentTeam()->s3s;
+ if ($this->definedS3s->count() > 0) {
+ $this->s3StorageId = $this->definedS3s->first()->id;
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
- public function submit(): void
+ public function submit()
{
try {
$this->validate();
+
$isValid = validate_cron_expression($this->frequency);
if (! $isValid) {
$this->dispatch('error', 'Invalid Cron / Human expression.');
return;
}
+
$payload = [
'enabled' => true,
'frequency' => $this->frequency,
- 'save_s3' => $this->save_s3,
- 's3_storage_id' => $this->s3_storage_id,
+ 'save_s3' => $this->saveToS3,
+ 's3_storage_id' => $this->s3StorageId,
'database_id' => $this->database->id,
'database_type' => $this->database->getMorphClass(),
'team_id' => currentTeam()->id,
];
+
if ($this->database->type() === 'standalone-postgresql') {
$payload['databases_to_backup'] = $this->database->postgres_db;
} elseif ($this->database->type() === 'standalone-mysql') {
@@ -71,11 +74,11 @@ class CreateScheduledBackup extends Component
} else {
$this->dispatch('refreshScheduledBackups');
}
+
} catch (\Throwable $e) {
- handleError($e, $this);
+ return handleError($e, $this);
} finally {
$this->frequency = '';
- $this->save_s3 = true;
}
}
}
diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php
index 394ba6c9a..ea6cd46b0 100644
--- a/app/Livewire/Project/Database/Dragonfly/General.php
+++ b/app/Livewire/Project/Database/Dragonfly/General.php
@@ -7,60 +7,111 @@ use App\Actions\Database\StopDatabaseProxy;
use App\Models\Server;
use App\Models\StandaloneDragonfly;
use Exception;
+use Illuminate\Support\Facades\Auth;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class General extends Component
{
- protected $listeners = ['refresh'];
-
public Server $server;
public StandaloneDragonfly $database;
- public ?string $db_url = null;
+ #[Validate(['required', 'string'])]
+ public string $name;
- public ?string $db_url_public = null;
+ #[Validate(['nullable', 'string'])]
+ public ?string $description = null;
- protected $rules = [
- 'database.name' => 'required',
- 'database.description' => 'nullable',
- 'database.dragonfly_password' => 'required',
- 'database.image' => 'required',
- 'database.ports_mappings' => 'nullable',
- 'database.is_public' => 'nullable|boolean',
- 'database.public_port' => 'nullable|integer',
- 'database.is_log_drain_enabled' => 'nullable|boolean',
- 'database.custom_docker_run_options' => 'nullable',
- ];
+ #[Validate(['required', 'string'])]
+ public string $dragonflyPassword;
- protected $validationAttributes = [
- 'database.name' => 'Name',
- 'database.description' => 'Description',
- 'database.dragonfly_password' => 'Redis Password',
- 'database.image' => 'Image',
- 'database.ports_mappings' => 'Port Mapping',
- 'database.is_public' => 'Is Public',
- 'database.public_port' => 'Public Port',
- 'database.custom_docker_run_options' => 'Custom Docker Run Options',
- ];
+ #[Validate(['required', 'string'])]
+ public string $image;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $portsMappings = null;
+
+ #[Validate(['nullable', 'boolean'])]
+ public ?bool $isPublic = null;
+
+ #[Validate(['nullable', 'integer'])]
+ public ?int $publicPort = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $customDockerRunOptions = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $dbUrl = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $dbUrlPublic = null;
+
+ #[Validate(['nullable', 'boolean'])]
+ public bool $isLogDrainEnabled = false;
+
+ public function getListeners()
+ {
+ $teamId = Auth::user()->currentTeam()->id;
+
+ return [
+ "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
+ ];
+ }
public function mount()
{
- $this->db_url = $this->database->internal_db_url;
- $this->db_url_public = $this->database->external_db_url;
- $this->server = data_get($this->database, 'destination.server');
+ try {
+ $this->syncData();
+ $this->server = data_get($this->database, 'destination.server');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->database->name = $this->name;
+ $this->database->description = $this->description;
+ $this->database->dragonfly_password = $this->dragonflyPassword;
+ $this->database->image = $this->image;
+ $this->database->ports_mappings = $this->portsMappings;
+ $this->database->is_public = $this->isPublic;
+ $this->database->public_port = $this->publicPort;
+ $this->database->custom_docker_run_options = $this->customDockerRunOptions;
+ $this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
+ $this->database->save();
+
+ $this->dbUrl = $this->database->internal_db_url;
+ $this->dbUrlPublic = $this->database->external_db_url;
+ } else {
+ $this->name = $this->database->name;
+ $this->description = $this->database->description;
+ $this->dragonflyPassword = $this->database->dragonfly_password;
+ $this->image = $this->database->image;
+ $this->portsMappings = $this->database->ports_mappings;
+ $this->isPublic = $this->database->is_public;
+ $this->publicPort = $this->database->public_port;
+ $this->customDockerRunOptions = $this->database->custom_docker_run_options;
+ $this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
+ $this->dbUrl = $this->database->internal_db_url;
+ $this->dbUrlPublic = $this->database->external_db_url;
+ }
}
public function instantSaveAdvanced()
{
try {
if (! $this->server->isLogDrainEnabled()) {
- $this->database->is_log_drain_enabled = false;
+ $this->isLogDrainEnabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
- $this->database->save();
+ $this->syncData(true);
+
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
@@ -68,11 +119,50 @@ class General extends Component
}
}
+ public function instantSave()
+ {
+ try {
+ if ($this->isPublic && ! $this->publicPort) {
+ $this->dispatch('error', 'Public port is required.');
+ $this->isPublic = false;
+
+ return;
+ }
+ if ($this->isPublic) {
+ if (! str($this->database->status)->startsWith('running')) {
+ $this->dispatch('error', 'Database must be started to be publicly accessible.');
+ $this->isPublic = false;
+
+ return;
+ }
+ StartDatabaseProxy::run($this->database);
+ $this->dispatch('success', 'Database is now publicly accessible.');
+ } else {
+ StopDatabaseProxy::run($this->database);
+ $this->dispatch('success', 'Database is no longer publicly accessible.');
+ }
+ $this->dbUrlPublic = $this->database->external_db_url;
+ $this->syncData(true);
+ } catch (\Throwable $e) {
+ $this->isPublic = ! $this->isPublic;
+ $this->syncData(true);
+
+ return handleError($e, $this);
+ }
+ }
+
+ public function databaseProxyStopped()
+ {
+ $this->syncData();
+ }
+
public function submit()
{
try {
- $this->validate();
- $this->database->save();
+ if (str($this->publicPort)->isEmpty()) {
+ $this->publicPort = null;
+ }
+ $this->syncData(true);
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
@@ -84,45 +174,4 @@ class General extends Component
}
}
}
-
- public function instantSave()
- {
- try {
- if ($this->database->is_public && ! $this->database->public_port) {
- $this->dispatch('error', 'Public port is required.');
- $this->database->is_public = false;
-
- return;
- }
- if ($this->database->is_public) {
- if (! str($this->database->status)->startsWith('running')) {
- $this->dispatch('error', 'Database must be started to be publicly accessible.');
- $this->database->is_public = false;
-
- return;
- }
- StartDatabaseProxy::run($this->database);
- $this->dispatch('success', 'Database is now publicly accessible.');
- } else {
- StopDatabaseProxy::run($this->database);
- $this->dispatch('success', 'Database is no longer publicly accessible.');
- }
- $this->db_url_public = $this->database->external_db_url;
- $this->database->save();
- } catch (\Throwable $e) {
- $this->database->is_public = ! $this->database->is_public;
-
- return handleError($e, $this);
- }
- }
-
- public function refresh(): void
- {
- $this->database->refresh();
- }
-
- public function render()
- {
- return view('livewire.project.database.dragonfly.general');
- }
}
diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php
index 49884ff9a..fc0febd02 100644
--- a/app/Livewire/Project/Database/Heading.php
+++ b/app/Livewire/Project/Database/Heading.php
@@ -6,6 +6,7 @@ use App\Actions\Database\RestartDatabase;
use App\Actions\Database\StartDatabase;
use App\Actions\Database\StopDatabase;
use App\Actions\Docker\GetContainersStatus;
+use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class Heading extends Component
@@ -18,7 +19,7 @@ class Heading extends Component
public function getListeners()
{
- $userId = auth()->user()->id;
+ $userId = Auth::id();
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => 'activityFinished',
diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php
index 663e7a6d7..062f454b1 100644
--- a/app/Livewire/Project/Database/Import.php
+++ b/app/Livewire/Project/Database/Import.php
@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Database;
use App\Models\Server;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
@@ -46,7 +47,7 @@ class Import extends Component
public function getListeners()
{
- $userId = auth()->user()->id;
+ $userId = Auth::id();
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
diff --git a/app/Livewire/Project/Database/InitScript.php b/app/Livewire/Project/Database/InitScript.php
index 336762981..e3baa1c8e 100644
--- a/app/Livewire/Project/Database/InitScript.php
+++ b/app/Livewire/Project/Database/InitScript.php
@@ -3,39 +3,39 @@
namespace App\Livewire\Project\Database;
use Exception;
+use Livewire\Attributes\Locked;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class InitScript extends Component
{
+ #[Locked]
public array $script;
+ #[Locked]
public int $index;
- public ?string $filename;
+ #[Validate(['nullable', 'string'])]
+ public ?string $filename = null;
- public ?string $content;
-
- protected $rules = [
- 'filename' => 'required|string',
- 'content' => 'required|string',
- ];
-
- protected $validationAttributes = [
- 'filename' => 'Filename',
- 'content' => 'Content',
- ];
+ #[Validate(['nullable', 'string'])]
+ public ?string $content = null;
public function mount()
{
- $this->index = data_get($this->script, 'index');
- $this->filename = data_get($this->script, 'filename');
- $this->content = data_get($this->script, 'content');
+ try {
+ $this->index = data_get($this->script, 'index');
+ $this->filename = data_get($this->script, 'filename');
+ $this->content = data_get($this->script, 'content');
+ } catch (Exception $e) {
+ return handleError($e, $this);
+ }
}
public function submit()
{
- $this->validate();
try {
+ $this->validate();
$this->script['index'] = $this->index;
$this->script['content'] = $this->content;
$this->script['filename'] = $this->filename;
@@ -47,6 +47,10 @@ class InitScript extends Component
public function delete()
{
- $this->dispatch('delete_init_script', $this->script);
+ try {
+ $this->dispatch('delete_init_script', $this->script);
+ } catch (Exception $e) {
+ return handleError($e, $this);
+ }
}
}
diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php
index df04f70d7..e768495eb 100644
--- a/app/Livewire/Project/Database/Keydb/General.php
+++ b/app/Livewire/Project/Database/Keydb/General.php
@@ -7,62 +7,116 @@ use App\Actions\Database\StopDatabaseProxy;
use App\Models\Server;
use App\Models\StandaloneKeydb;
use Exception;
+use Illuminate\Support\Facades\Auth;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class General extends Component
{
- protected $listeners = ['refresh'];
-
public Server $server;
public StandaloneKeydb $database;
- public ?string $db_url = null;
+ #[Validate(['required', 'string'])]
+ public string $name;
- public ?string $db_url_public = null;
+ #[Validate(['nullable', 'string'])]
+ public ?string $description = null;
- protected $rules = [
- 'database.name' => 'required',
- 'database.description' => 'nullable',
- 'database.keydb_conf' => 'nullable',
- 'database.keydb_password' => 'required',
- 'database.image' => 'required',
- 'database.ports_mappings' => 'nullable',
- 'database.is_public' => 'nullable|boolean',
- 'database.public_port' => 'nullable|integer',
- 'database.is_log_drain_enabled' => 'nullable|boolean',
- 'database.custom_docker_run_options' => 'nullable',
- ];
+ #[Validate(['nullable', 'string'])]
+ public ?string $keydbConf = null;
- protected $validationAttributes = [
- 'database.name' => 'Name',
- 'database.description' => 'Description',
- 'database.keydb_conf' => 'Redis Configuration',
- 'database.keydb_password' => 'Redis Password',
- 'database.image' => 'Image',
- 'database.ports_mappings' => 'Port Mapping',
- 'database.is_public' => 'Is Public',
- 'database.public_port' => 'Public Port',
- 'database.custom_docker_run_options' => 'Custom Docker Run Options',
- ];
+ #[Validate(['required', 'string'])]
+ public string $keydbPassword;
+
+ #[Validate(['required', 'string'])]
+ public string $image;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $portsMappings = null;
+
+ #[Validate(['nullable', 'boolean'])]
+ public ?bool $isPublic = null;
+
+ #[Validate(['nullable', 'integer'])]
+ public ?int $publicPort = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $customDockerRunOptions = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $dbUrl = null;
+
+ #[Validate(['nullable', 'string'])]
+ public ?string $dbUrlPublic = null;
+
+ #[Validate(['nullable', 'boolean'])]
+ public bool $isLogDrainEnabled = false;
+
+ public function getListeners()
+ {
+ $teamId = Auth::user()->currentTeam()->id;
+
+ return [
+ "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
+ ];
+ }
public function mount()
{
- $this->db_url = $this->database->internal_db_url;
- $this->db_url_public = $this->database->external_db_url;
- $this->server = data_get($this->database, 'destination.server');
+ try {
+ $this->syncData();
+ $this->server = data_get($this->database, 'destination.server');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->database->name = $this->name;
+ $this->database->description = $this->description;
+ $this->database->keydb_conf = $this->keydbConf;
+ $this->database->keydb_password = $this->keydbPassword;
+ $this->database->image = $this->image;
+ $this->database->ports_mappings = $this->portsMappings;
+ $this->database->is_public = $this->isPublic;
+ $this->database->public_port = $this->publicPort;
+ $this->database->custom_docker_run_options = $this->customDockerRunOptions;
+ $this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
+ $this->database->save();
+
+ $this->dbUrl = $this->database->internal_db_url;
+ $this->dbUrlPublic = $this->database->external_db_url;
+ } else {
+ $this->name = $this->database->name;
+ $this->description = $this->database->description;
+ $this->keydbConf = $this->database->keydb_conf;
+ $this->keydbPassword = $this->database->keydb_password;
+ $this->image = $this->database->image;
+ $this->portsMappings = $this->database->ports_mappings;
+ $this->isPublic = $this->database->is_public;
+ $this->publicPort = $this->database->public_port;
+ $this->customDockerRunOptions = $this->database->custom_docker_run_options;
+ $this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
+ $this->dbUrl = $this->database->internal_db_url;
+ $this->dbUrlPublic = $this->database->external_db_url;
+ }
}
public function instantSaveAdvanced()
{
try {
if (! $this->server->isLogDrainEnabled()) {
- $this->database->is_log_drain_enabled = false;
+ $this->isLogDrainEnabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
- $this->database->save();
+ $this->syncData(true);
+
$this->dispatch('success', 'Database updated.');
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
@@ -70,14 +124,50 @@ class General extends Component
}
}
+ public function instantSave()
+ {
+ try {
+ if ($this->isPublic && ! $this->publicPort) {
+ $this->dispatch('error', 'Public port is required.');
+ $this->isPublic = false;
+
+ return;
+ }
+ if ($this->isPublic) {
+ if (! str($this->database->status)->startsWith('running')) {
+ $this->dispatch('error', 'Database must be started to be publicly accessible.');
+ $this->isPublic = false;
+
+ return;
+ }
+ StartDatabaseProxy::run($this->database);
+ $this->dispatch('success', 'Database is now publicly accessible.');
+ } else {
+ StopDatabaseProxy::run($this->database);
+ $this->dispatch('success', 'Database is no longer publicly accessible.');
+ }
+ $this->dbUrlPublic = $this->database->external_db_url;
+ $this->syncData(true);
+ } catch (\Throwable $e) {
+ $this->isPublic = ! $this->isPublic;
+ $this->syncData(true);
+
+ return handleError($e, $this);
+ }
+ }
+
+ public function databaseProxyStopped()
+ {
+ $this->syncData();
+ }
+
public function submit()
{
try {
- $this->validate();
- if ($this->database->keydb_conf === '') {
- $this->database->keydb_conf = null;
+ if (str($this->publicPort)->isEmpty()) {
+ $this->publicPort = null;
}
- $this->database->save();
+ $this->syncData(true);
$this->dispatch('success', 'Database updated.');
} catch (Exception $e) {
return handleError($e, $this);
@@ -89,45 +179,4 @@ class General extends Component
}
}
}
-
- public function instantSave()
- {
- try {
- if ($this->database->is_public && ! $this->database->public_port) {
- $this->dispatch('error', 'Public port is required.');
- $this->database->is_public = false;
-
- return;
- }
- if ($this->database->is_public) {
- if (! str($this->database->status)->startsWith('running')) {
- $this->dispatch('error', 'Database must be started to be publicly accessible.');
- $this->database->is_public = false;
-
- return;
- }
- StartDatabaseProxy::run($this->database);
- $this->dispatch('success', 'Database is now publicly accessible.');
- } else {
- StopDatabaseProxy::run($this->database);
- $this->dispatch('success', 'Database is no longer publicly accessible.');
- }
- $this->db_url_public = $this->database->external_db_url;
- $this->database->save();
- } catch (\Throwable $e) {
- $this->database->is_public = ! $this->database->is_public;
-
- return handleError($e, $this);
- }
- }
-
- public function refresh(): void
- {
- $this->database->refresh();
- }
-
- public function render()
- {
- return view('livewire.project.database.keydb.general');
- }
}
diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php
index 6d8c3aff7..1ee5de269 100644
--- a/app/Livewire/Project/DeleteEnvironment.php
+++ b/app/Livewire/Project/DeleteEnvironment.php
@@ -37,6 +37,6 @@ class DeleteEnvironment extends Component
return redirect()->route('project.show', parameters: ['project_uuid' => $this->parameters['project_uuid']]);
}
- return $this->dispatch('error', 'Environment has defined resources, please delete them first.');
+ return $this->dispatch('error', "Environment {$environment->name} has defined resources, please delete them first.");
}
}
diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php
index 360fad10a..f320a19b0 100644
--- a/app/Livewire/Project/DeleteProject.php
+++ b/app/Livewire/Project/DeleteProject.php
@@ -27,11 +27,12 @@ class DeleteProject extends Component
'project_id' => 'required|int',
]);
$project = Project::findOrFail($this->project_id);
- if ($project->applications->count() > 0) {
- return $this->dispatch('error', 'Project has resources defined, please delete them first.');
- }
- $project->delete();
+ if ($project->isEmpty()) {
+ $project->delete();
- return redirect()->route('project.index');
+ return redirect()->route('project.index');
+ }
+
+ return $this->dispatch('error', "Project {$project->name} has resources defined, please delete them first.");
}
}
diff --git a/app/Livewire/Project/Edit.php b/app/Livewire/Project/Edit.php
index 62c1bfc11..463febb10 100644
--- a/app/Livewire/Project/Edit.php
+++ b/app/Livewire/Project/Edit.php
@@ -3,17 +3,17 @@
namespace App\Livewire\Project;
use App\Models\Project;
-use Livewire\Attributes\Rule;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class Edit extends Component
{
public Project $project;
- #[Rule(['required', 'string', 'min:3', 'max:255'])]
+ #[Validate(['required', 'string', 'min:3', 'max:255'])]
public string $name;
- #[Rule(['nullable', 'string', 'max:255'])]
+ #[Validate(['nullable', 'string', 'max:255'])]
public ?string $description = null;
public function mount(string $project_uuid)
diff --git a/app/Livewire/Project/EnvironmentEdit.php b/app/Livewire/Project/EnvironmentEdit.php
index fc33cf6b6..f48220b3d 100644
--- a/app/Livewire/Project/EnvironmentEdit.php
+++ b/app/Livewire/Project/EnvironmentEdit.php
@@ -5,7 +5,7 @@ namespace App\Livewire\Project;
use App\Models\Application;
use App\Models\Project;
use Livewire\Attributes\Locked;
-use Livewire\Attributes\Rule;
+use Livewire\Attributes\Validate;
use Livewire\Component;
class EnvironmentEdit extends Component
@@ -17,10 +17,10 @@ class EnvironmentEdit extends Component
#[Locked]
public $environment;
- #[Rule(['required', 'string', 'min:3', 'max:255'])]
+ #[Validate(['required', 'string', 'min:3', 'max:255'])]
public string $name;
- #[Rule(['nullable', 'string', 'max:255'])]
+ #[Validate(['nullable', 'string', 'max:255'])]
public ?string $description = null;
public function mount(string $project_uuid, string $environment_name)
diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php
index 7f8247597..0b6d075a4 100644
--- a/app/Livewire/Project/New/Select.php
+++ b/app/Livewire/Project/New/Select.php
@@ -91,9 +91,16 @@ class Select extends Component
{
$services = get_service_templates(true);
$services = collect($services)->map(function ($service, $key) {
+ $default_logo = 'images/default.webp';
+ $logo = data_get($service, 'logo', $default_logo);
+ $local_logo_path = public_path($logo);
+
return [
'name' => str($key)->headline(),
- 'logo' => asset(data_get($service, 'logo', 'svgs/coolify.png')),
+ 'logo' => asset($logo),
+ 'logo_github_url' => file_exists($local_logo_path)
+ ? 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/'.$logo
+ : asset($default_logo),
] + (array) $service;
})->all();
$gitBasedApplications = [
@@ -141,14 +148,14 @@ class Select extends Component
'id' => 'postgresql',
'name' => 'PostgreSQL',
'description' => 'PostgreSQL is an object-relational database known for its robustness, advanced features, and strong standards compliance.',
- 'logo' => '
+ 'logo' => '
',
],
[
'id' => 'mysql',
'name' => 'MySQL',
'description' => 'MySQL is an open-source relational database management system. ',
- 'logo' => '