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 -![image](https://github.com/user-attachments/assets/c95a07df-7c5a-4e77-a35a-81f25fcbece1) +![image](https://github.com/user-attachments/assets/726fb63e-c3b8-4260-b3ac-06780605ec5d) * [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! Paweł Pierścionek Michael Mazurczak Formbricks -Adith Suhas +StartupFame +Jonas Jaeger +JP +Evercam +Web3 Career ## 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 coollabsio%2Fcoolify | Trendshift +# Core Maintainers + +| Andras Bacsai | 🏔️ Peak | +|------------|------------| +| Andras Bacsai | peaklabs-dev | +| | | + # Repo Activity ![Alt](https://repobeats.axiom.co/api/embed/eab1c8066f9c59d0ad37b76c23ebb5ccac4278ae.svg "Repobeats analytics image") 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' => ' + 'logo' => ' @@ -158,38 +165,38 @@ class Select extends Component [ 'id' => 'mariadb', 'name' => 'MariaDB', - 'description' => 'MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system, intended to remain free and open-source software under the GNU General Public License.', - 'logo' => '', + 'description' => 'MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system, intended to remain free and open-source.', + 'logo' => '', ], [ 'id' => 'redis', 'name' => 'Redis', 'description' => 'Redis is a source-available, in-memory storage, used as a distributed, in-memory key–value database, cache and message broker, with optional durability.', - 'logo' => '', + 'logo' => '', ], [ 'id' => 'keydb', 'name' => 'KeyDB', 'description' => 'KeyDB is a database that offers high performance, low latency, and scalability for various data structures and workloads.', - 'logo' => '
', + 'logo' => '
', ], [ 'id' => 'dragonfly', 'name' => 'Dragonfly', 'description' => 'Dragonfly DB is a drop-in Redis replacement that delivers 25x more throughput and 12x faster snapshotting than Redis.', - 'logo' => '
', + 'logo' => '
', ], [ 'id' => 'mongodb', 'name' => 'MongoDB', 'description' => 'MongoDB is a source-available, cross-platform, document-oriented database program.', - 'logo' => '', + 'logo' => '', ], [ 'id' => 'clickhouse', 'name' => 'ClickHouse', 'description' => 'ClickHouse is a column-oriented database that supports real-time analytics, business intelligence, observability, ML and GenAI, and more.', - 'logo' => '
', + 'logo' => '
', ], ]; @@ -326,7 +333,7 @@ class Select extends Component public function loadServers() { - $this->servers = Server::isUsable()->get(); + $this->servers = Server::isUsable()->get()->sortBy('name'); $this->allServers = $this->servers; } } diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php index a2e48fee7..319ead361 100644 --- a/app/Livewire/Project/Service/Configuration.php +++ b/app/Livewire/Project/Service/Configuration.php @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Service; use App\Actions\Docker\GetContainersStatus; use App\Models\Service; +use Illuminate\Support\Facades\Auth; use Livewire\Component; class Configuration extends Component @@ -20,7 +21,7 @@ class Configuration extends Component public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'check_status', diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index 67b575599..ee43dc911 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -7,6 +7,7 @@ use App\Actions\Service\StopService; use App\Actions\Shared\PullImage; use App\Events\ServiceStatusChanged; use App\Models\Service; +use Illuminate\Support\Facades\Auth; use Livewire\Component; use Spatie\Activitylog\Models\Activity; @@ -34,7 +35,7 @@ class Navbar extends Component public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted', diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index e71cd9f42..6294d97c6 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -88,6 +88,9 @@ class Show extends Component public function lock() { $this->env->is_shown_once = true; + if ($this->isSharedVariable) { + unset($this->env->is_required); + } $this->serialize(); $this->env->save(); $this->checkEnvs(); diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 621ab1bac..d12d8e26a 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -168,18 +168,42 @@ class ExecuteContainerCommand extends Component return; } try { + // Validate container name format + if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $this->selected_container)) { + throw new \InvalidArgumentException('Invalid container name format'); + } + + // Verify container exists in our allowed list $container = collect($this->containers)->firstWhere('container.Names', $this->selected_container); if (is_null($container)) { throw new \RuntimeException('Container not found.'); } - $server = data_get($this->container, 'server'); + + // Verify server ownership and status + $server = data_get($container, 'server'); + if (! $server || ! $server instanceof Server) { + throw new \RuntimeException('Invalid server configuration.'); + } if ($server->isForceDisabled()) { throw new \RuntimeException('Server is disabled.'); } + + // Additional ownership verification based on resource type + $resourceServer = match ($this->type) { + 'application' => $this->resource->destination->server, + 'database' => $this->resource->destination->server, + 'service' => $this->resource->server, + default => throw new \RuntimeException('Invalid resource type.') + }; + + if ($server->id !== $resourceServer->id && ! $this->resource->additional_servers->contains('id', $server->id)) { + throw new \RuntimeException('Server ownership verification failed.'); + } + $this->dispatch( 'send-terminal-command', - isset($container), + true, data_get($container, 'container.Names'), data_get($container, 'server.uuid') ); diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php index 05d9a7a13..0710e37ff 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php @@ -2,23 +2,60 @@ namespace App\Livewire\Project\Shared\ScheduledTask; +use App\Models\ScheduledTask; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Locked; use Livewire\Component; class Executions extends Component { - public $executions = []; + public ScheduledTask $task; - public $selectedKey; + #[Locked] + public int $taskId; - public $task; + #[Locked] + public Collection $executions; + + #[Locked] + public ?int $selectedKey = null; + + #[Locked] + public ?string $serverTimezone = null; public function getListeners() { + $teamId = Auth::user()->currentTeam()->id; + return [ - 'selectTask', + "echo-private:team.{$teamId},ScheduledTaskDone" => 'refreshExecutions', ]; } + public function mount($taskId) + { + try { + $this->taskId = $taskId; + $this->task = ScheduledTask::findOrFail($taskId); + $this->executions = $this->task->executions()->take(20)->get(); + $this->serverTimezone = data_get($this->task, 'application.destination.server.settings.server_timezone'); + if (! $this->serverTimezone) { + $this->serverTimezone = data_get($this->task, 'service.destination.server.settings.server_timezone'); + } + if (! $this->serverTimezone) { + $this->serverTimezone = 'UTC'; + } + } catch (\Exception $e) { + return handleError($e); + } + } + + public function refreshExecutions(): void + { + $this->executions = $this->task->executions()->take(20)->get(); + } + public function selectTask($key): void { if ($key == $this->selectedKey) { @@ -29,38 +66,9 @@ class Executions extends Component $this->selectedKey = $key; } - public function server() - { - if (! $this->task) { - return null; - } - - if ($this->task->application) { - if ($this->task->application->destination && $this->task->application->destination->server) { - return $this->task->application->destination->server; - } - } elseif ($this->task->service) { - if ($this->task->service->destination && $this->task->service->destination->server) { - return $this->task->service->destination->server; - } - } - - return null; - } - - public function getServerTimezone() - { - $server = $this->server(); - if (! $server) { - return 'UTC'; - } - - return $server->settings->server_timezone; - } - public function formatDateInServerTimezone($date) { - $serverTimezone = $this->getServerTimezone(); + $serverTimezone = $this->serverTimezone; $dateObj = new \DateTime($date); try { $dateObj->setTimezone(new \DateTimeZone($serverTimezone)); diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php index 36194edb7..0900a1d70 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Show.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php @@ -2,74 +2,124 @@ namespace App\Livewire\Project\Shared\ScheduledTask; +use App\Jobs\ScheduledTaskJob; use App\Models\Application; -use App\Models\ScheduledTask as ModelsScheduledTask; +use App\Models\ScheduledTask; use App\Models\Service; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; -use Visus\Cuid2\Cuid2; class Show extends Component { - public $parameters; - public Application|Service $resource; - public ModelsScheduledTask $task; + public ScheduledTask $task; - public ?string $modalId = null; + #[Locked] + public array $parameters; + #[Locked] public string $type; - public string $scheduledTaskName; + #[Validate(['boolean'])] + public bool $isEnabled = false; - protected $rules = [ - 'task.enabled' => 'required|boolean', - 'task.name' => 'required|string', - 'task.command' => 'required|string', - 'task.frequency' => 'required|string', - 'task.container' => 'nullable|string', - ]; + #[Validate(['string', 'required'])] + public string $name; - protected $validationAttributes = [ - 'name' => 'name', - 'command' => 'command', - 'frequency' => 'frequency', - 'container' => 'container', - ]; + #[Validate(['string', 'required'])] + public string $command; - public function mount() + #[Validate(['string', 'required'])] + public string $frequency; + + #[Validate(['string', 'nullable'])] + public ?string $container = null; + + #[Locked] + public ?string $application_uuid; + + #[Locked] + public ?string $service_uuid; + + #[Locked] + public string $task_uuid; + + public function mount(string $task_uuid, string $project_uuid, string $environment_name, ?string $application_uuid = null, ?string $service_uuid = null) { - $this->parameters = get_route_parameters(); + try { + $this->task_uuid = $task_uuid; + if ($application_uuid) { + $this->type = 'application'; + $this->application_uuid = $application_uuid; + $this->resource = Application::ownedByCurrentTeam()->where('uuid', $application_uuid)->firstOrFail(); + } elseif ($service_uuid) { + $this->type = 'service'; + $this->service_uuid = $service_uuid; + $this->resource = Service::ownedByCurrentTeam()->where('uuid', $service_uuid)->firstOrFail(); + } + $this->parameters = [ + 'environment_name' => $environment_name, + 'project_uuid' => $project_uuid, + 'application_uuid' => $application_uuid, + 'service_uuid' => $service_uuid, + ]; - if (data_get($this->parameters, 'application_uuid')) { - $this->type = 'application'; - $this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail(); - } elseif (data_get($this->parameters, 'service_uuid')) { - $this->type = 'service'; - $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); + $this->task = $this->resource->scheduled_tasks()->where('uuid', $task_uuid)->firstOrFail(); + $this->syncData(); + } catch (\Exception $e) { + return handleError($e); } + } - $this->modalId = new Cuid2; - $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first(); - $this->scheduledTaskName = $this->task->name; + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->task->enabled = $this->isEnabled; + $this->task->name = str($this->name)->trim()->value(); + $this->task->command = str($this->command)->trim()->value(); + $this->task->frequency = str($this->frequency)->trim()->value(); + $this->task->container = str($this->container)->trim()->value(); + $this->task->save(); + } else { + $this->isEnabled = $this->task->enabled; + $this->name = $this->task->name; + $this->command = $this->task->command; + $this->frequency = $this->task->frequency; + $this->container = $this->task->container; + } } public function instantSave() { - $this->validateOnly('task.enabled'); - $this->task->save(['enabled' => $this->task->enabled]); - $this->dispatch('success', 'Scheduled task updated.'); - $this->dispatch('refreshTasks'); + try { + $this->syncData(true); + $this->dispatch('success', 'Scheduled task updated.'); + $this->refreshTasks(); + } catch (\Exception $e) { + return handleError($e); + } } public function submit() { - $this->validate(); - $this->task->name = str($this->task->name)->trim()->value(); - $this->task->container = str($this->task->container)->trim()->value(); - $this->task->save(); - $this->dispatch('success', 'Scheduled task updated.'); - $this->dispatch('refreshTasks'); + try { + $this->syncData(true); + $this->dispatch('success', 'Scheduled task updated.'); + } catch (\Exception $e) { + return handleError($e); + } + } + + public function refreshTasks() + { + try { + $this->task->refresh(); + } catch (\Exception $e) { + return handleError($e); + } } public function delete() @@ -78,12 +128,22 @@ class Show extends Component $this->task->delete(); if ($this->type === 'application') { - return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName); + return redirect()->route('project.application.configuration', $this->parameters, $this->task->name); } else { - return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName); + return redirect()->route('project.service.configuration', $this->parameters, $this->task->name); } } catch (\Exception $e) { return handleError($e); } } + + public function executeNow() + { + try { + ScheduledTaskJob::dispatch($this->task); + $this->dispatch('success', 'Scheduled task executed.'); + } catch (\Exception $e) { + return handleError($e); + } + } } diff --git a/app/Livewire/Project/Shared/Tags.php b/app/Livewire/Project/Shared/Tags.php index dca6180ff..811859cb8 100644 --- a/app/Livewire/Project/Shared/Tags.php +++ b/app/Livewire/Project/Shared/Tags.php @@ -37,6 +37,7 @@ class Tags extends Component $this->validate(); $tags = str($this->newTags)->trim()->explode(' '); foreach ($tags as $tag) { + $tag = strip_tags($tag); if (strlen($tag) < 2) { $this->dispatch('error', 'Invalid tag.', "Tag $tag is invalid. Min length is 2."); @@ -65,6 +66,7 @@ class Tags extends Component public function addTag(string $id, string $name) { try { + $name = strip_tags($name); if ($this->resource->tags()->where('id', $id)->exists()) { $this->dispatch('error', 'Duplicate tags.', "Tag $name already added."); diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php index 5af8f057e..d8f101277 100644 --- a/app/Livewire/Project/Shared/Terminal.php +++ b/app/Livewire/Project/Shared/Terminal.php @@ -29,11 +29,20 @@ class Terminal extends Component $server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail(); if ($isContainer) { + // Validate container identifier format (alphanumeric, dashes, and underscores only) + if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $identifier)) { + throw new \InvalidArgumentException('Invalid container identifier format'); + } + + // Verify container exists and belongs to the user's team $status = getContainerStatus($server, $identifier); if ($status !== 'running') { return; } - $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); + + // Escape the identifier for shell usage + $escapedIdentifier = escapeshellarg($identifier); + $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$escapedIdentifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); } else { $command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi'); } diff --git a/app/Livewire/Project/Show.php b/app/Livewire/Project/Show.php index 8374a98cc..2335519c7 100644 --- a/app/Livewire/Project/Show.php +++ b/app/Livewire/Project/Show.php @@ -4,17 +4,17 @@ namespace App\Livewire\Project; use App\Models\Environment; use App\Models\Project; -use Livewire\Attributes\Rule; +use Livewire\Attributes\Validate; use Livewire\Component; class Show extends Component { public Project $project; - #[Rule(['required', 'string', 'min:3'])] + #[Validate(['required', 'string', 'min:3'])] public string $name; - #[Rule(['nullable', 'string'])] + #[Validate(['nullable', 'string'])] public ?string $description = null; public function mount(string $project_uuid) diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php index becae9f04..0650de9a0 100644 --- a/app/Livewire/Server/Advanced.php +++ b/app/Livewire/Server/Advanced.php @@ -4,7 +4,7 @@ namespace App\Livewire\Server; use App\Jobs\DockerCleanupJob; use App\Models\Server; -use Livewire\Attributes\Rule; +use Livewire\Attributes\Validate; use Livewire\Component; class Advanced extends Component @@ -13,28 +13,28 @@ class Advanced extends Component public array $parameters = []; - #[Rule(['integer', 'min:1'])] + #[Validate(['integer', 'min:1'])] public int $concurrentBuilds = 1; - #[Rule(['integer', 'min:1'])] + #[Validate(['integer', 'min:1'])] public int $dynamicTimeout = 1; - #[Rule('boolean')] + #[Validate('boolean')] public bool $forceDockerCleanup = false; - #[Rule('string')] + #[Validate(['string', 'required'])] public string $dockerCleanupFrequency = '*/10 * * * *'; - #[Rule(['integer', 'min:1', 'max:99'])] + #[Validate(['integer', 'min:1', 'max:99'])] public int $dockerCleanupThreshold = 10; - #[Rule(['integer', 'min:1', 'max:99'])] + #[Validate(['integer', 'min:1', 'max:99'])] public int $serverDiskUsageNotificationThreshold = 50; - #[Rule('boolean')] + #[Validate('boolean')] public bool $deleteUnusedVolumes = false; - #[Rule('boolean')] + #[Validate('boolean')] public bool $deleteUnusedNetworks = false; public function mount(string $server_uuid) @@ -78,7 +78,6 @@ class Advanced extends Component try { $this->syncData(true); $this->dispatch('success', 'Server updated.'); - // $this->dispatch('refreshServerShow'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/CloudflareTunnels.php b/app/Livewire/Server/CloudflareTunnels.php index f16962bca..f69fc8655 100644 --- a/app/Livewire/Server/CloudflareTunnels.php +++ b/app/Livewire/Server/CloudflareTunnels.php @@ -3,20 +3,23 @@ namespace App\Livewire\Server; use App\Models\Server; -use Livewire\Attributes\Rule; +use Livewire\Attributes\Validate; use Livewire\Component; class CloudflareTunnels extends Component { public Server $server; - #[Rule(['required', 'boolean'])] + #[Validate(['required', 'boolean'])] public bool $isCloudflareTunnelsEnabled; public function mount(string $server_uuid) { try { $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + if ($this->server->isLocalhost()) { + return redirect()->route('server.show', ['server_uuid' => $server_uuid]); + } $this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel; } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Server/Destinations.php b/app/Livewire/Server/Destinations.php index c10958bd1..dbab6e03f 100644 --- a/app/Livewire/Server/Destinations.php +++ b/app/Livewire/Server/Destinations.php @@ -19,7 +19,6 @@ class Destinations extends Component try { $this->networks = collect(); $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); - loggy($this->server); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php deleted file mode 100644 index 740421373..000000000 --- a/app/Livewire/Server/Form.php +++ /dev/null @@ -1,281 +0,0 @@ -user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'cloudflareTunnelConfigured', - 'refreshServerShow' => 'serverInstalled', - 'revalidate' => '$refresh', - ]; - } - - protected $rules = [ - 'server.name' => 'required', - 'server.description' => 'nullable', - 'server.ip' => 'required', - 'server.user' => 'required', - 'server.port' => 'required', - 'wildcard_domain' => 'nullable|url', - 'server.settings.is_reachable' => 'required', - 'server.settings.is_swarm_manager' => 'required|boolean', - 'server.settings.is_swarm_worker' => 'required|boolean', - 'server.settings.is_build_server' => 'required|boolean', - 'server.settings.is_metrics_enabled' => 'required|boolean', - 'server.settings.sentinel_token' => 'required', - 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'required|integer|min:1', - 'server.settings.sentinel_metrics_history_days' => 'required|integer|min:1', - 'server.settings.sentinel_push_interval_seconds' => 'required|integer|min:10', - 'server.settings.sentinel_custom_url' => 'nullable|url', - 'server.settings.is_sentinel_enabled' => 'required|boolean', - 'server.settings.is_sentinel_debug_enabled' => 'required|boolean', - 'server.settings.server_timezone' => 'required|string|timezone', - ]; - - protected $validationAttributes = [ - 'server.name' => 'Name', - 'server.description' => 'Description', - 'server.ip' => 'IP address/Domain', - 'server.user' => 'User', - 'server.port' => 'Port', - 'server.settings.is_reachable' => 'Is reachable', - 'server.settings.is_swarm_manager' => 'Swarm Manager', - 'server.settings.is_swarm_worker' => 'Swarm Worker', - 'server.settings.is_build_server' => 'Build Server', - 'server.settings.is_metrics_enabled' => 'Metrics', - 'server.settings.sentinel_token' => 'Metrics Token', - 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'Metrics Interval', - 'server.settings.sentinel_metrics_history_days' => 'Metrics History', - 'server.settings.sentinel_push_interval_seconds' => 'Push Interval', - 'server.settings.is_sentinel_enabled' => 'Server API', - 'server.settings.is_sentinel_debug_enabled' => 'Debug', - 'server.settings.sentinel_custom_url' => 'Coolify URL', - 'server.settings.server_timezone' => 'Server Timezone', - ]; - - public function mount(Server $server) - { - $this->server = $server; - $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); - $this->wildcard_domain = $this->server->settings->wildcard_domain; - } - - public function checkSyncStatus() - { - $this->server->refresh(); - $this->server->settings->refresh(); - } - - public function regenerateSentinelToken() - { - try { - $this->server->settings->generateSentinelToken(); - $this->server->settings->refresh(); - // $this->restartSentinel(notification: false); - $this->dispatch('success', 'Token regenerated & Sentinel restarted.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function updated($field) - { - if ($field === 'server.settings.docker_cleanup_frequency') { - $frequency = $this->server->settings->docker_cleanup_frequency; - if (! validate_cron_expression($frequency)) { - $this->dispatch('error', 'Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.'); - $this->server->settings->docker_cleanup_frequency = '*/10 * * * *'; - } - } - } - - public function cloudflareTunnelConfigured() - { - $this->serverInstalled(); - $this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); - } - - public function serverInstalled() - { - $this->server->refresh(); - $this->server->settings->refresh(); - } - - public function updatedServerSettingsIsBuildServer() - { - $this->dispatch('refreshServerShow'); - $this->dispatch('serverRefresh'); - $this->dispatch('proxyStatusUpdated'); - } - - public function updatedServerSettingsIsSentinelEnabled($value) - { - $this->validate([ - 'server.settings.sentinel_custom_url' => 'required|url', - ]); - if ($value === false) { - StopSentinel::dispatch($this->server); - $this->server->settings->is_metrics_enabled = false; - $this->server->settings->save(); - $this->server->sentinelHeartbeat(isReset: true); - } else { - try { - StartSentinel::run($this->server); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - } - - public function updatedServerSettingsIsMetricsEnabled() - { - $this->restartSentinel(); - } - - public function updatedServerSettingsIsSentinelDebugEnabled() - { - $this->restartSentinel(); - } - - public function instantSave() - { - try { - $this->validate(); - refresh_server_connection($this->server->privateKey); - $this->validateServer(false); - - $this->server->settings->save(); - $this->server->save(); - $this->dispatch('success', 'Server updated.'); - $this->dispatch('refreshServerShow'); - } catch (\Throwable $e) { - $this->server->settings->refresh(); - - return handleError($e, $this); - } finally { - } - } - - public function saveSentinel() - { - try { - $this->validate(); - $this->server->settings->save(); - $this->dispatch('success', 'Sentinel updated.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } finally { - $this->checkSyncStatus(); - } - } - - public function restartSentinel($notification = true) - { - try { - $this->validate(); - $this->validate([ - 'server.settings.sentinel_custom_url' => 'required|url', - ]); - $this->server->settings->save(); - $this->server->restartSentinel(async: false); - if ($notification) { - $this->dispatch('success', 'Sentinel restarted.'); - } - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function revalidate() - { - $this->revalidate = true; - } - - public function checkLocalhostConnection() - { - $this->submit(); - ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); - if ($uptime) { - $this->dispatch('success', 'Server is reachable.'); - $this->server->settings->is_reachable = true; - $this->server->settings->is_usable = true; - $this->server->settings->save(); - $this->dispatch('proxyStatusUpdated'); - } else { - $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.

Check this documentation for further help.

Error: '.$error); - - return; - } - } - - public function validateServer($install = true) - { - $this->server->update([ - 'validation_logs' => null, - ]); - $this->dispatch('init', $install); - } - - public function submit() - { - try { - if (isCloud() && ! isDev()) { - $this->validate(); - $this->validate([ - 'server.ip' => 'required', - ]); - } else { - $this->validate(); - } - $uniqueIPs = Server::all()->reject(function (Server $server) { - return $server->id === $this->server->id; - })->pluck('ip')->toArray(); - if (in_array($this->server->ip, $uniqueIPs)) { - $this->dispatch('error', 'IP address is already in use by another team.'); - - return; - } - refresh_server_connection($this->server->privateKey); - $this->server->settings->wildcard_domain = $this->wildcard_domain; - $currentTimezone = $this->server->settings->getOriginal('server_timezone'); - $newTimezone = $this->server->settings->server_timezone; - if ($currentTimezone !== $newTimezone || $currentTimezone === '') { - $this->server->settings->server_timezone = $newTimezone; - } - $this->server->settings->save(); - $this->server->save(); - - $this->dispatch('success', 'Server updated.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } -} diff --git a/app/Livewire/Server/LogDrains.php b/app/Livewire/Server/LogDrains.php index fb8ef329f..edddfc755 100644 --- a/app/Livewire/Server/LogDrains.php +++ b/app/Livewire/Server/LogDrains.php @@ -5,38 +5,38 @@ namespace App\Livewire\Server; use App\Actions\Server\StartLogDrain; use App\Actions\Server\StopLogDrain; use App\Models\Server; -use Livewire\Attributes\Rule; +use Livewire\Attributes\Validate; use Livewire\Component; class LogDrains extends Component { public Server $server; - #[Rule(['boolean'])] + #[Validate(['boolean'])] public bool $isLogDrainNewRelicEnabled = false; - #[Rule(['boolean'])] + #[Validate(['boolean'])] public bool $isLogDrainCustomEnabled = false; - #[Rule(['boolean'])] + #[Validate(['boolean'])] public bool $isLogDrainAxiomEnabled = false; - #[Rule(['string', 'nullable'])] + #[Validate(['string', 'nullable'])] public ?string $logDrainNewRelicLicenseKey = null; - #[Rule(['url', 'nullable'])] + #[Validate(['url', 'nullable'])] public ?string $logDrainNewRelicBaseUri = null; - #[Rule(['string', 'nullable'])] + #[Validate(['string', 'nullable'])] public ?string $logDrainAxiomDatasetName = null; - #[Rule(['string', 'nullable'])] + #[Validate(['string', 'nullable'])] public ?string $logDrainAxiomApiKey = null; - #[Rule(['string', 'nullable'])] + #[Validate(['string', 'nullable'])] public ?string $logDrainCustomConfig = null; - #[Rule(['string', 'nullable'])] + #[Validate(['string', 'nullable'])] public ?string $logDrainCustomConfigParser = null; public function mount(string $server_uuid) @@ -49,36 +49,114 @@ class LogDrains extends Component } } - public function syncData(bool $toModel = false) + public function syncDataNewRelic(bool $toModel = false) { if ($toModel) { - $this->validate(); $this->server->settings->is_logdrain_newrelic_enabled = $this->isLogDrainNewRelicEnabled; - $this->server->settings->is_logdrain_axiom_enabled = $this->isLogDrainAxiomEnabled; - $this->server->settings->is_logdrain_custom_enabled = $this->isLogDrainCustomEnabled; - $this->server->settings->logdrain_newrelic_license_key = $this->logDrainNewRelicLicenseKey; $this->server->settings->logdrain_newrelic_base_uri = $this->logDrainNewRelicBaseUri; - $this->server->settings->logdrain_axiom_dataset_name = $this->logDrainAxiomDatasetName; - $this->server->settings->logdrain_axiom_api_key = $this->logDrainAxiomApiKey; - $this->server->settings->logdrain_custom_config = $this->logDrainCustomConfig; - $this->server->settings->logdrain_custom_config_parser = $this->logDrainCustomConfigParser; - - $this->server->settings->save(); } else { $this->isLogDrainNewRelicEnabled = $this->server->settings->is_logdrain_newrelic_enabled; - $this->isLogDrainAxiomEnabled = $this->server->settings->is_logdrain_axiom_enabled; - $this->isLogDrainCustomEnabled = $this->server->settings->is_logdrain_custom_enabled; - $this->logDrainNewRelicLicenseKey = $this->server->settings->logdrain_newrelic_license_key; $this->logDrainNewRelicBaseUri = $this->server->settings->logdrain_newrelic_base_uri; + } + } + + public function syncDataAxiom(bool $toModel = false) + { + if ($toModel) { + $this->server->settings->is_logdrain_axiom_enabled = $this->isLogDrainAxiomEnabled; + $this->server->settings->logdrain_axiom_dataset_name = $this->logDrainAxiomDatasetName; + $this->server->settings->logdrain_axiom_api_key = $this->logDrainAxiomApiKey; + } else { + $this->isLogDrainAxiomEnabled = $this->server->settings->is_logdrain_axiom_enabled; $this->logDrainAxiomDatasetName = $this->server->settings->logdrain_axiom_dataset_name; $this->logDrainAxiomApiKey = $this->server->settings->logdrain_axiom_api_key; + } + } + + public function syncDataCustom(bool $toModel = false) + { + if ($toModel) { + $this->server->settings->is_logdrain_custom_enabled = $this->isLogDrainCustomEnabled; + $this->server->settings->logdrain_custom_config = $this->logDrainCustomConfig; + $this->server->settings->logdrain_custom_config_parser = $this->logDrainCustomConfigParser; + } else { + $this->isLogDrainCustomEnabled = $this->server->settings->is_logdrain_custom_enabled; $this->logDrainCustomConfig = $this->server->settings->logdrain_custom_config; $this->logDrainCustomConfigParser = $this->server->settings->logdrain_custom_config_parser; } } + public function syncData(bool $toModel = false, ?string $type = null) + { + if ($toModel) { + $this->customValidation(); + if ($type === 'newrelic') { + $this->syncDataNewRelic($toModel); + } elseif ($type === 'axiom') { + $this->syncDataAxiom($toModel); + } elseif ($type === 'custom') { + $this->syncDataCustom($toModel); + } else { + $this->syncDataNewRelic($toModel); + $this->syncDataAxiom($toModel); + $this->syncDataCustom($toModel); + } + $this->server->settings->save(); + } else { + if ($type === 'newrelic') { + $this->syncDataNewRelic($toModel); + } elseif ($type === 'axiom') { + $this->syncDataAxiom($toModel); + } elseif ($type === 'custom') { + $this->syncDataCustom($toModel); + } else { + $this->syncDataNewRelic($toModel); + $this->syncDataAxiom($toModel); + $this->syncDataCustom($toModel); + } + } + } + + public function customValidation() + { + if ($this->isLogDrainNewRelicEnabled) { + try { + $this->validate([ + 'logDrainNewRelicLicenseKey' => ['required'], + 'logDrainNewRelicBaseUri' => ['required', 'url'], + ]); + } catch (\Throwable $e) { + $this->isLogDrainNewRelicEnabled = false; + + throw $e; + } + } elseif ($this->isLogDrainAxiomEnabled) { + try { + $this->validate([ + 'logDrainAxiomDatasetName' => ['required'], + 'logDrainAxiomApiKey' => ['required'], + ]); + } catch (\Throwable $e) { + $this->isLogDrainAxiomEnabled = false; + + throw $e; + } + } elseif ($this->isLogDrainCustomEnabled) { + try { + $this->validate([ + 'logDrainCustomConfig' => ['required'], + 'logDrainCustomConfigParser' => ['string', 'nullable'], + ]); + } catch (\Throwable $e) { + $this->isLogDrainCustomEnabled = false; + + throw $e; + } + } + } + public function instantSave() { try { @@ -98,7 +176,7 @@ class LogDrains extends Component public function submit(string $type) { try { - $this->syncData(true); + $this->syncData(true, $type); $this->dispatch('success', 'Settings saved.'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php index f80152435..5f60c5db5 100644 --- a/app/Livewire/Server/New/ByIp.php +++ b/app/Livewire/Server/New/ByIp.php @@ -6,64 +6,60 @@ use App\Enums\ProxyTypes; use App\Models\Server; use App\Models\Team; use Illuminate\Support\Collection; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class ByIp extends Component { + #[Locked] public $private_keys; + #[Locked] public $limit_reached; + #[Validate('nullable|integer', as: 'Private Key')] public ?int $private_key_id = null; + #[Validate('nullable|string', as: 'Private Key Name')] public $new_private_key_name; + #[Validate('nullable|string', as: 'Private Key Description')] public $new_private_key_description; + #[Validate('nullable|string', as: 'Private Key Value')] public $new_private_key_value; + #[Validate('required|string', as: 'Name')] public string $name; + #[Validate('nullable|string', as: 'Description')] public ?string $description = null; + #[Validate('required|string', as: 'IP Address/Domain')] public string $ip; + #[Validate('required|string', as: 'User')] public string $user = 'root'; + #[Validate('required|integer|between:1,65535', as: 'Port')] public int $port = 22; + #[Validate('required|boolean', as: 'Swarm Manager')] public bool $is_swarm_manager = false; + #[Validate('required|boolean', as: 'Swarm Worker')] public bool $is_swarm_worker = false; + #[Validate('nullable|integer', as: 'Swarm Cluster')] public $selected_swarm_cluster = null; + #[Validate('required|boolean', as: 'Build Server')] public bool $is_build_server = false; + #[Locked] public Collection $swarm_managers; - protected $rules = [ - 'name' => 'required|string', - 'description' => 'nullable|string', - 'ip' => 'required', - 'user' => 'required|string', - 'port' => 'required|integer', - 'is_swarm_manager' => 'required|boolean', - 'is_swarm_worker' => 'required|boolean', - 'is_build_server' => 'required|boolean', - ]; - - protected $validationAttributes = [ - 'name' => 'Name', - 'description' => 'Description', - 'ip' => 'IP Address/Domain', - 'user' => 'User', - 'port' => 'Port', - 'is_swarm_manager' => 'Swarm Manager', - 'is_swarm_worker' => 'Swarm Worker', - 'is_build_server' => 'Build Server', - ]; - public function mount() { $this->name = generate_random_name(); @@ -88,6 +84,12 @@ class ByIp extends Component { $this->validate(); try { + if (Server::where('team_id', currentTeam()->id) + ->where('ip', $this->ip) + ->exists()) { + return $this->dispatch('error', 'This IP/Domain is already in use by another server in your team.'); + } + if (is_null($this->private_key_id)) { return $this->dispatch('error', 'You must select a private key'); } diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 94ea3509a..4e325c1ff 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -4,7 +4,6 @@ namespace App\Livewire\Server; use App\Actions\Proxy\CheckConfiguration; use App\Actions\Proxy\SaveConfiguration; -use App\Actions\Proxy\StartProxy; use App\Models\Server; use Livewire\Component; @@ -16,6 +15,8 @@ class Proxy extends Component public $proxy_settings = null; + public bool $redirect_enabled = true; + public ?string $redirect_url = null; protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit']; @@ -27,6 +28,7 @@ class Proxy extends Component public function mount() { $this->selectedProxy = $this->server->proxyType(); + $this->redirect_enabled = data_get($this->server, 'proxy.redirect_enabled', true); $this->redirect_url = data_get($this->server, 'proxy.redirect_url'); } @@ -39,19 +41,18 @@ class Proxy extends Component { $this->server->proxy = null; $this->server->save(); - $this->dispatch('proxyChanged'); + $this->dispatch('reloadWindow'); } public function selectProxy($proxy_type) { - $this->server->proxy->set('status', 'exited'); - $this->server->proxy->set('type', $proxy_type); - $this->server->save(); - $this->selectedProxy = $this->server->proxy->type; - if ($this->server->proxySet()) { - StartProxy::run($this->server, false); + try { + $this->server->changeProxy($proxy_type, async: false); + $this->selectedProxy = $this->server->proxy->type; + $this->dispatch('reloadWindow'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->dispatch('proxyStatusUpdated'); } public function instantSave() @@ -65,13 +66,25 @@ class Proxy extends Component } } + public function instantSaveRedirect() + { + try { + $this->server->proxy->redirect_enabled = $this->redirect_enabled; + $this->server->save(); + $this->server->setupDefaultRedirect(); + $this->dispatch('success', 'Proxy configuration saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function submit() { try { SaveConfiguration::run($this->server, $this->proxy_settings); $this->server->proxy->redirect_url = $this->redirect_url; $this->server->save(); - $this->server->setupDefault404Redirect(); + $this->server->setupDefaultRedirect(); $this->dispatch('success', 'Proxy configuration saved.'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php index 8fcff85d6..4f9d41092 100644 --- a/app/Livewire/Server/Proxy/Deploy.php +++ b/app/Livewire/Server/Proxy/Deploy.php @@ -65,7 +65,7 @@ class Deploy extends Component public function restart() { try { - $this->stop(forceStop: false); + $this->stop(); $this->dispatch('checkProxy'); } catch (\Throwable $e) { return handleError($e, $this); @@ -105,6 +105,7 @@ class Deploy extends Component $startTime = Carbon::now()->getTimestamp(); while ($process->running()) { + ray('running'); if (Carbon::now()->getTimestamp() - $startTime >= $timeout) { $this->forceStopContainer($containerName); break; diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 2e4b67cc6..ac5211c1b 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -5,88 +5,87 @@ namespace App\Livewire\Server; use App\Actions\Server\StartSentinel; use App\Actions\Server\StopSentinel; use App\Models\Server; -use Livewire\Attributes\Rule; +use Livewire\Attributes\Computed; +use Livewire\Attributes\Validate; use Livewire\Component; class Show extends Component { public Server $server; - #[Rule(['required'])] + #[Validate(['required'])] public string $name; - #[Rule(['nullable'])] - public ?string $description; + #[Validate(['nullable'])] + public ?string $description = null; - #[Rule(['required'])] + #[Validate(['required'])] public string $ip; - #[Rule(['required'])] + #[Validate(['required'])] public string $user; - #[Rule(['required'])] + #[Validate(['required'])] public string $port; - #[Rule(['nullable'])] + #[Validate(['nullable'])] public ?string $validationLogs = null; - #[Rule(['nullable', 'url'])] - public ?string $wildcardDomain; + #[Validate(['nullable', 'url'])] + public ?string $wildcardDomain = null; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isReachable; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isUsable; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isSwarmManager; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isSwarmWorker; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isBuildServer; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isMetricsEnabled; - #[Rule(['required'])] + #[Validate(['required'])] public string $sentinelToken; - #[Rule(['nullable'])] - public ?string $sentinelUpdatedAt; + #[Validate(['nullable'])] + public ?string $sentinelUpdatedAt = null; - #[Rule(['required', 'integer', 'min:1'])] + #[Validate(['required', 'integer', 'min:1'])] public int $sentinelMetricsRefreshRateSeconds; - #[Rule(['required', 'integer', 'min:1'])] + #[Validate(['required', 'integer', 'min:1'])] public int $sentinelMetricsHistoryDays; - #[Rule(['required', 'integer', 'min:10'])] + #[Validate(['required', 'integer', 'min:10'])] public int $sentinelPushIntervalSeconds; - #[Rule(['nullable', 'url'])] - public ?string $sentinelCustomUrl; + #[Validate(['nullable', 'url'])] + public ?string $sentinelCustomUrl = null; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isSentinelEnabled; - #[Rule(['required'])] + #[Validate(['required'])] public bool $isSentinelDebugEnabled; - #[Rule(['required'])] + #[Validate(['required'])] public string $serverTimezone; - public array $timezones; - public function getListeners() { $teamId = auth()->user()->currentTeam()->id; return [ - "echo-private:team.{$teamId},CloudflareTunnelConfigured" => '$refresh', - 'refreshServerShow' => '$refresh', + "echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh', + 'refreshServerShow' => 'refresh', ]; } @@ -94,17 +93,34 @@ class Show extends Component { try { $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); - $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); $this->syncData(); } catch (\Throwable $e) { return handleError($e, $this); } } + #[Computed] + public function timezones(): array + { + return collect(timezone_identifiers_list()) + ->sort() + ->values() + ->toArray(); + } + public function syncData(bool $toModel = false) { if ($toModel) { $this->validate(); + + if (Server::where('team_id', currentTeam()->id) + ->where('ip', $this->ip) + ->where('id', '!=', $this->server->id) + ->exists()) { + $this->ip = $this->server->ip; + throw new \Exception('This IP/Domain is already in use by another server in your team.'); + } + $this->server->name = $this->name; $this->server->description = $this->description; $this->server->ip = $this->ip; @@ -114,6 +130,7 @@ class Show extends Component $this->server->save(); $this->server->settings->is_swarm_manager = $this->isSwarmManager; + $this->server->settings->wildcard_domain = $this->wildcardDomain; $this->server->settings->is_swarm_worker = $this->isSwarmWorker; $this->server->settings->is_build_server = $this->isBuildServer; $this->server->settings->is_metrics_enabled = $this->isMetricsEnabled; @@ -124,7 +141,14 @@ class Show extends Component $this->server->settings->sentinel_custom_url = $this->sentinelCustomUrl; $this->server->settings->is_sentinel_enabled = $this->isSentinelEnabled; $this->server->settings->is_sentinel_debug_enabled = $this->isSentinelDebugEnabled; - $this->server->settings->server_timezone = $this->serverTimezone; + + if (! validate_timezone($this->serverTimezone)) { + $this->serverTimezone = config('app.timezone'); + throw new \Exception('Invalid timezone.'); + } else { + $this->server->settings->server_timezone = $this->serverTimezone; + } + $this->server->settings->save(); } else { $this->name = $this->server->name; @@ -132,6 +156,7 @@ class Show extends Component $this->ip = $this->server->ip; $this->user = $this->server->user; $this->port = $this->server->port; + $this->wildcardDomain = $this->server->settings->wildcard_domain; $this->isReachable = $this->server->settings->is_reachable; $this->isUsable = $this->server->settings->is_usable; @@ -151,6 +176,12 @@ class Show extends Component } } + public function refresh() + { + $this->syncData(); + $this->dispatch('$refresh'); + } + public function validateServer($install = true) { try { diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php index 8c5bc23ed..791ef9350 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -159,7 +159,8 @@ class ValidateAndInstall extends Component $this->dispatch('refreshBoardingIndex'); $this->dispatch('success', 'Server validated.'); } else { - $this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: documentation.'; + $requiredDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.'); + $this->error = 'Minimum Docker Engine version '.$requiredDockerVersion.' is not instaled. Please install Docker manually before continuing: documentation.'; $this->server->update([ 'validation_logs' => $this->error, ]); diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index 0a6c5bae6..c1be35ced 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -7,8 +7,8 @@ use App\Models\InstanceSettings; use App\Models\Server; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; -use Livewire\Attributes\Locked; -use Livewire\Attributes\Rule; +use Livewire\Attributes\Computed; +use Livewire\Attributes\Validate; use Livewire\Component; class Index extends Component @@ -17,61 +17,58 @@ class Index extends Component protected Server $server; - #[Locked] - public $timezones; - - #[Rule('boolean')] + #[Validate('boolean')] public bool $is_auto_update_enabled; - #[Rule('nullable|string|max:255')] + #[Validate('nullable|string|max:255')] public ?string $fqdn = null; - #[Rule('nullable|string|max:255')] + #[Validate('nullable|string|max:255')] public ?string $resale_license = null; - #[Rule('required|integer|min:1025|max:65535')] + #[Validate('required|integer|min:1025|max:65535')] public int $public_port_min; - #[Rule('required|integer|min:1025|max:65535')] + #[Validate('required|integer|min:1025|max:65535')] public int $public_port_max; - #[Rule('nullable|string')] + #[Validate('nullable|string')] public ?string $custom_dns_servers = null; - #[Rule('nullable|string|max:255')] + #[Validate('nullable|string|max:255')] public ?string $instance_name = null; - #[Rule('nullable|string')] + #[Validate('nullable|string')] public ?string $allowed_ips = null; - #[Rule('nullable|string')] + #[Validate('nullable|string')] public ?string $public_ipv4 = null; - #[Rule('nullable|string')] + #[Validate('nullable|string')] public ?string $public_ipv6 = null; - #[Rule('string')] + #[Validate('string')] public string $auto_update_frequency; - #[Rule('string')] + #[Validate('string|required')] public string $update_check_frequency; - #[Rule('required|string|timezone')] + #[Validate('required|string|timezone')] public string $instance_timezone; - #[Rule('boolean')] + #[Validate('boolean')] public bool $do_not_track; - #[Rule('boolean')] + #[Validate('boolean')] public bool $is_registration_enabled; - #[Rule('boolean')] + #[Validate('boolean')] public bool $is_dns_validation_enabled; - #[Rule('boolean')] + #[Validate('boolean')] public bool $is_api_enabled; - #[Rule('boolean')] + #[Validate('boolean')] public bool $disable_two_step_confirmation; public function render() @@ -101,14 +98,29 @@ class Index extends Component $this->is_api_enabled = $this->settings->is_api_enabled; $this->auto_update_frequency = $this->settings->auto_update_frequency; $this->update_check_frequency = $this->settings->update_check_frequency; - $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); $this->instance_timezone = $this->settings->instance_timezone; $this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation; } } + #[Computed] + public function timezones(): array + { + return collect(timezone_identifiers_list()) + ->sort() + ->values() + ->toArray(); + } + public function instantSave($isSave = true) { + $this->validate(); + if ($this->settings->is_auto_update_enabled === true) { + $this->validate([ + 'auto_update_frequency' => ['required', 'string'], + ]); + } + $this->settings->fqdn = $this->fqdn; $this->settings->resale_license = $this->resale_license; $this->settings->public_port_min = $this->public_port_min; @@ -139,6 +151,14 @@ class Index extends Component $error_show = false; $this->server = Server::findOrFail(0); $this->resetErrorBag(); + + if (! validate_timezone($this->instance_timezone)) { + $this->instance_timezone = config('app.timezone'); + throw new \Exception('Invalid timezone.'); + } else { + $this->settings->instance_timezone = $this->instance_timezone; + } + if ($this->settings->public_port_min > $this->settings->public_port_max) { $this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.'); diff --git a/app/Livewire/Settings/License.php b/app/Livewire/Settings/License.php deleted file mode 100644 index 79f8269f3..000000000 --- a/app/Livewire/Settings/License.php +++ /dev/null @@ -1,58 +0,0 @@ - 'nullable', - 'settings.is_resale_license_active' => 'nullable', - ]; - - protected $validationAttributes = [ - 'settings.resale_license' => 'License', - 'instance_id' => 'Instance Id (Do not change this)', - 'settings.is_resale_license_active' => 'Is License Active', - ]; - - public function mount() - { - if (! isCloud()) { - abort(404); - } - if (! isInstanceAdmin()) { - return redirect()->route('home'); - } - $this->instance_id = config('app.id'); - $this->settings = instanceSettings(); - } - - public function render() - { - return view('livewire.settings.license'); - } - - public function submit() - { - $this->validate(); - $this->settings->save(); - if ($this->settings->resale_license) { - try { - CheckResaleLicense::run(); - $this->dispatch('reloadWindow'); - } catch (\Throwable $e) { - session()->flash('error', 'Something went wrong. Please contact support.
Error: '.$e->getMessage()); - - return redirect()->route('settings.license'); - } - } - } -} diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index 38f7e548a..1b0599ffe 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -8,7 +8,7 @@ use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandalonePostgresql; use Livewire\Attributes\Locked; -use Livewire\Attributes\Rule; +use Livewire\Attributes\Validate; use Livewire\Component; class SettingsBackup extends Component @@ -25,19 +25,19 @@ class SettingsBackup extends Component #[Locked] public $executions = []; - #[Rule(['required'])] + #[Validate(['required'])] public string $uuid; - #[Rule(['required'])] + #[Validate(['required'])] public string $name; - #[Rule(['nullable'])] + #[Validate(['nullable'])] public ?string $description = null; - #[Rule(['required'])] + #[Validate(['required'])] public string $postgres_user; - #[Rule(['required'])] + #[Validate(['required'])] public string $postgres_password; public function mount() @@ -99,6 +99,14 @@ class SettingsBackup extends Component $this->database->refresh(); $this->backup->refresh(); $this->s3s = S3Storage::whereTeamId(0)->get(); + + $this->uuid = $this->database->uuid; + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->postgres_user = $this->database->postgres_user; + $this->postgres_password = $this->database->postgres_password; + $this->executions = $this->backup->executions; + } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index 2a017ed34..abf3a12f9 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -3,44 +3,44 @@ namespace App\Livewire; use App\Models\InstanceSettings; -use Livewire\Attributes\Rule; +use Livewire\Attributes\Validate; use Livewire\Component; class SettingsEmail extends Component { public InstanceSettings $settings; - #[Rule(['boolean'])] + #[Validate(['boolean'])] public bool $smtpEnabled = false; - #[Rule(['nullable', 'string'])] + #[Validate(['nullable', 'string'])] public ?string $smtpHost = null; - #[Rule(['nullable', 'numeric', 'min:1', 'max:65535'])] + #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])] public ?int $smtpPort = null; - #[Rule(['nullable', 'string'])] + #[Validate(['nullable', 'string', 'in:tls,ssl,none'])] public ?string $smtpEncryption = null; - #[Rule(['nullable', 'string'])] + #[Validate(['nullable', 'string'])] public ?string $smtpUsername = null; - #[Rule(['nullable'])] + #[Validate(['nullable'])] public ?string $smtpPassword = null; - #[Rule(['nullable', 'numeric'])] + #[Validate(['nullable', 'numeric'])] public ?int $smtpTimeout = null; - #[Rule(['nullable', 'email'])] + #[Validate(['nullable', 'email'])] public ?string $smtpFromAddress = null; - #[Rule(['nullable', 'string'])] + #[Validate(['nullable', 'string'])] public ?string $smtpFromName = null; - #[Rule(['boolean'])] + #[Validate(['boolean'])] public bool $resendEnabled = false; - #[Rule(['nullable', 'string'])] + #[Validate(['nullable', 'string'])] public ?string $resendApiKey = null; public function mount() @@ -63,6 +63,8 @@ class SettingsEmail extends Component $this->settings->smtp_username = $this->smtpUsername; $this->settings->smtp_password = $this->smtpPassword; $this->settings->smtp_timeout = $this->smtpTimeout; + $this->settings->smtp_from_address = $this->smtpFromAddress; + $this->settings->smtp_from_name = $this->smtpFromName; $this->settings->resend_enabled = $this->resendEnabled; $this->settings->resend_api_key = $this->resendApiKey; diff --git a/app/Livewire/SharedVariables/Environment/Show.php b/app/Livewire/SharedVariables/Environment/Show.php index daf1df212..6a33eb60d 100644 --- a/app/Livewire/SharedVariables/Environment/Show.php +++ b/app/Livewire/SharedVariables/Environment/Show.php @@ -16,7 +16,7 @@ class Show extends Component public array $parameters; - protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey']; + protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey', 'environmentVariableDeleted' => '$refresh']; public function saveKey($data) { diff --git a/app/Livewire/SharedVariables/Project/Show.php b/app/Livewire/SharedVariables/Project/Show.php index 8d4844442..0171283c4 100644 --- a/app/Livewire/SharedVariables/Project/Show.php +++ b/app/Livewire/SharedVariables/Project/Show.php @@ -9,7 +9,7 @@ class Show extends Component { public Project $project; - protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey']; + protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey', 'environmentVariableDeleted' => '$refresh']; public function saveKey($data) { diff --git a/app/Livewire/SharedVariables/Team/Index.php b/app/Livewire/SharedVariables/Team/Index.php index a3085304a..a76ccf58a 100644 --- a/app/Livewire/SharedVariables/Team/Index.php +++ b/app/Livewire/SharedVariables/Team/Index.php @@ -9,7 +9,7 @@ class Index extends Component { public Team $team; - protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey']; + protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey', 'environmentVariableDeleted' => '$refresh']; public function saveKey($data) { diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 07cef54f9..467927484 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -4,6 +4,11 @@ namespace App\Livewire\Source\Github; use App\Jobs\GithubAppPermissionJob; use App\Models\GithubApp; +use App\Models\PrivateKey; +use Illuminate\Support\Facades\Http; +use Lcobucci\JWT\Configuration; +use Lcobucci\JWT\Signer\Key\InMemory; +use Lcobucci\JWT\Signer\Rsa\Sha256; use Livewire\Component; class Change extends Component @@ -51,12 +56,20 @@ class Change extends Component 'github_app.administration' => 'nullable|string', ]; + public function boot() + { + if ($this->github_app) { + $this->github_app->makeVisible(['client_secret', 'webhook_secret']); + } + } + public function checkPermissions() { GithubAppPermissionJob::dispatchSync($this->github_app); $this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->dispatch('success', 'Github App permissions updated.'); } + // public function check() // { @@ -90,15 +103,16 @@ class Change extends Component // ray($runners_by_repository); // } + public function mount() { try { $github_app_uuid = request()->github_app_uuid; $this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail(); + $this->github_app->makeVisible(['client_secret', 'webhook_secret']); $this->applications = $this->github_app->applications; $settings = instanceSettings(); - $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->name = str($this->github_app->name)->kebab(); $this->fqdn = $settings->fqdn; @@ -142,6 +156,77 @@ class Change extends Component } } + public function getGithubAppNameUpdatePath() + { + if (str($this->github_app->organization)->isNotEmpty()) { + return "{$this->github_app->html_url}/organizations/{$this->github_app->organization}/settings/apps/{$this->github_app->name}"; + } + + return "{$this->github_app->html_url}/settings/apps/{$this->github_app->name}"; + } + + private function generateGithubJwt($private_key, $app_id): string + { + $configuration = Configuration::forAsymmetricSigner( + new Sha256, + InMemory::plainText($private_key), + InMemory::plainText($private_key) + ); + + $now = time(); + + return $configuration->builder() + ->issuedBy((string) $app_id) + ->permittedFor('https://api.github.com') + ->identifiedBy((string) $now) + ->issuedAt(new \DateTimeImmutable("@{$now}")) + ->expiresAt(new \DateTimeImmutable('@'.($now + 600))) + ->getToken($configuration->signer(), $configuration->signingKey()) + ->toString(); + } + + public function updateGithubAppName() + { + try { + $privateKey = PrivateKey::ownedByCurrentTeam()->find($this->github_app->private_key_id); + + if (! $privateKey) { + $this->dispatch('error', 'No private key found for this GitHub App.'); + + return; + } + + $jwt = $this->generateGithubJwt($privateKey->private_key, $this->github_app->app_id); + + $response = Http::withHeaders([ + 'Accept' => 'application/vnd.github+json', + 'X-GitHub-Api-Version' => '2022-11-28', + 'Authorization' => "Bearer {$jwt}", + ])->get("{$this->github_app->api_url}/app"); + + if ($response->successful()) { + $app_data = $response->json(); + $app_slug = $app_data['slug'] ?? null; + + if ($app_slug) { + $this->github_app->name = $app_slug; + $this->name = str($app_slug)->kebab(); + $privateKey->name = "github-app-{$app_slug}"; + $privateKey->save(); + $this->github_app->save(); + $this->dispatch('success', 'GitHub App name and SSH key name synchronized successfully.'); + } else { + $this->dispatch('info', 'Could not find App Name (slug) in GitHub response.'); + } + } else { + $error_message = $response->json()['message'] ?? 'Unknown error'; + $this->dispatch('error', "Failed to fetch GitHub App information: {$error_message}"); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function submit() { try { diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php index 103c5c9fb..136d3525e 100644 --- a/app/Livewire/Source/Github/Create.php +++ b/app/Livewire/Source/Github/Create.php @@ -23,7 +23,7 @@ class Create extends Component public function mount() { - $this->name = substr(generate_random_name(), 0, 34); // GitHub Apps names can only be 34 characters long + $this->name = substr(generate_random_name(), 0, 30); } public function createGitHubApp() diff --git a/app/Livewire/Subscription/PricingPlans.php b/app/Livewire/Subscription/PricingPlans.php index 9186cc978..6b2d3fb36 100644 --- a/app/Livewire/Subscription/PricingPlans.php +++ b/app/Livewire/Subscription/PricingPlans.php @@ -2,6 +2,7 @@ namespace App\Livewire\Subscription; +use Illuminate\Support\Facades\Auth; use Livewire\Component; use Stripe\Checkout\Session; use Stripe\Stripe; @@ -26,7 +27,7 @@ class PricingPlans extends Component $payload = [ 'allow_promotion_codes' => true, 'billing_address_collection' => 'required', - 'client_reference_id' => auth()->user()->id.':'.currentTeam()->id, + 'client_reference_id' => Auth::id().':'.currentTeam()->id, 'line_items' => [[ 'price' => $priceId, 'adjustable_quantity' => [ @@ -43,7 +44,7 @@ class PricingPlans extends Component ], 'subscription_data' => [ 'metadata' => [ - 'user_id' => auth()->user()->id, + 'user_id' => Auth::id(), 'team_id' => currentTeam()->id, ], ], @@ -60,7 +61,7 @@ class PricingPlans extends Component 'name' => 'auto', ]; } else { - $payload['customer_email'] = auth()->user()->email; + $payload['customer_email'] = Auth::user()->email; } $session = Session::create($payload); diff --git a/app/Livewire/Tags/Index.php b/app/Livewire/Tags/Index.php deleted file mode 100644 index 116f19e4e..000000000 --- a/app/Livewire/Tags/Index.php +++ /dev/null @@ -1,82 +0,0 @@ - 'updateDeployments']; - - public function render() - { - return view('livewire.tags.index'); - } - - public function mount() - { - $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); - if ($this->tag) { - $this->tagUpdated(); - } - } - - public function updateDeployments($deployments) - { - $this->deploymentsPerTagPerServer = $deployments; - } - - public function tagUpdated() - { - if ($this->tag === '') { - return; - } - $sanitizedTag = htmlspecialchars($this->tag, ENT_QUOTES, 'UTF-8'); - $tag = $this->tags->where('name', $sanitizedTag)->first(); - if (! $tag) { - $this->dispatch('error', 'Tag ('.e($sanitizedTag).') not found.'); - $this->tag = ''; - - return; - } - $this->webhook = generateTagDeployWebhook($tag->name); - $this->applications = $tag->applications()->get(); - $this->services = $tag->services()->get(); - } - - public function redeployAll() - { - try { - $this->applications->each(function ($resource) { - $deploy = new DeployController; - $deploy->deploy_resource($resource); - }); - $this->services->each(function ($resource) { - $deploy = new DeployController; - $deploy->deploy_resource($resource); - }); - $this->dispatch('success', 'Mass deployment started.'); - } catch (\Exception $e) { - return handleError($e, $this); - } - } -} diff --git a/app/Livewire/Tags/Show.php b/app/Livewire/Tags/Show.php index 0dffcce57..fc5b13374 100644 --- a/app/Livewire/Tags/Show.php +++ b/app/Livewire/Tags/Show.php @@ -5,43 +5,57 @@ namespace App\Livewire\Tags; use App\Http\Controllers\Api\DeployController; use App\Models\ApplicationDeploymentQueue; use App\Models\Tag; +use Illuminate\Support\Collection; +use Livewire\Attributes\Locked; use Livewire\Attributes\Title; use Livewire\Component; #[Title('Tags | Coolify')] class Show extends Component { - public $tags; + #[Locked] + public ?string $tagName = null; - public Tag $tag; + #[Locked] + public ?Collection $tags = null; - public $applications; + #[Locked] + public ?Tag $tag = null; - public $services; + #[Locked] + public ?Collection $applications = null; - public $webhook = null; + #[Locked] + public ?Collection $services = null; - public $deployments_per_tag_per_server = []; + #[Locked] + public ?string $webhook = null; + + #[Locked] + public ?array $deploymentsPerTagPerServer = null; public function mount() { - $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); - $tag = $this->tags->where('name', request()->tag_name)->first(); - if (! $tag) { - return redirect()->route('tags.index'); + try { + $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); + if (str($this->tagName)->isNotEmpty()) { + $tag = $this->tags->where('name', $this->tagName)->first(); + $this->webhook = generateTagDeployWebhook($tag->name); + $this->applications = $tag->applications()->get(); + $this->services = $tag->services()->get(); + $this->tag = $tag; + $this->getDeployments(); + } + } catch (\Exception $e) { + return handleError($e, $this); } - $this->webhook = generateTagDeployWebhook($tag->name); - $this->applications = $tag->applications()->get(); - $this->services = $tag->services()->get(); - $this->tag = $tag; - $this->get_deployments(); } - public function get_deployments() + public function getDeployments() { try { $resource_ids = $this->applications->pluck('id'); - $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([ + $this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([ 'id', 'application_id', 'application_name', @@ -56,7 +70,7 @@ class Show extends Component } } - public function redeploy_all() + public function redeployAll() { try { $message = collect([]); diff --git a/app/Livewire/Team/Create.php b/app/Livewire/Team/Create.php index 992833da5..f805d6122 100644 --- a/app/Livewire/Team/Create.php +++ b/app/Livewire/Team/Create.php @@ -3,28 +3,21 @@ namespace App\Livewire\Team; use App\Models\Team; +use Livewire\Attributes\Validate; use Livewire\Component; class Create extends Component { + #[Validate(['required', 'min:3', 'max:255'])] public string $name = ''; + #[Validate(['nullable', 'min:3', 'max:255'])] public ?string $description = null; - protected $rules = [ - 'name' => 'required|min:3|max:255', - 'description' => 'nullable|min:3|max:255', - ]; - - protected $validationAttributes = [ - 'name' => 'name', - 'description' => 'description', - ]; - public function submit() { - $this->validate(); try { + $this->validate(); $team = Team::create([ 'name' => $this->name, 'description' => $this->description, diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php index 45600dbfe..0972e7364 100644 --- a/app/Livewire/Team/Index.php +++ b/app/Livewire/Team/Index.php @@ -4,6 +4,7 @@ namespace App\Livewire\Team; use App\Models\Team; use App\Models\TeamInvitation; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Livewire\Component; @@ -55,7 +56,7 @@ class Index 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/Terminal/Index.php b/app/Livewire/Terminal/Index.php index 945b25714..a24a237c5 100644 --- a/app/Livewire/Terminal/Index.php +++ b/app/Livewire/Terminal/Index.php @@ -14,13 +14,25 @@ class Index extends Component public $containers = []; + public bool $isLoadingContainers = true; + public function mount() { if (! auth()->user()->isAdmin()) { abort(403); } $this->servers = Server::isReachable()->get(); - $this->containers = $this->getAllActiveContainers(); + } + + public function loadContainers() + { + try { + $this->containers = $this->getAllActiveContainers(); + } catch (\Exception $e) { + return handleError($e, $this); + } finally { + $this->isLoadingContainers = false; + } } private function getAllActiveContainers() diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php index 88ed88cb7..e50085c64 100644 --- a/app/Livewire/Upgrade.php +++ b/app/Livewire/Upgrade.php @@ -23,6 +23,9 @@ class Upgrade extends Component try { $this->latestVersion = get_latest_version_of_coolify(); $this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false); + if (isDev()) { + $this->isUpgradeAvailable = true; + } } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Waitlist/Index.php b/app/Livewire/Waitlist/Index.php index 422415449..0524b495c 100644 --- a/app/Livewire/Waitlist/Index.php +++ b/app/Livewire/Waitlist/Index.php @@ -27,7 +27,7 @@ class Index extends Component public function mount() { - if (config('coolify.waitlist') == false) { + if (config('constants.waitlist.enabled') == false) { return redirect()->route('register'); } $this->waitingInLine = Waitlist::whereVerified(true)->count(); diff --git a/app/Models/Application.php b/app/Models/Application.php index 91abf2e3a..d1efd3f33 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Enums\ApplicationDeploymentStatus; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Process\InvokedProcess; @@ -98,12 +99,13 @@ use Visus\Cuid2\Cuid2; 'updated_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the application was last updated.'], 'deleted_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'The date and time when the application was deleted.'], 'compose_parsing_version' => ['type' => 'string', 'description' => 'How Coolify parse the compose file.'], + 'custom_nginx_configuration' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom Nginx configuration base64 encoded.'], ] )] class Application extends BaseModel { - use SoftDeletes; + use HasFactory, SoftDeletes; private static $parserVersion = '4'; @@ -114,11 +116,11 @@ class Application extends BaseModel protected static function booted() { static::saving(function ($application) { - if ($application->fqdn === '') { - $application->fqdn = null; - } $payload = []; if ($application->isDirty('fqdn')) { + if ($application->fqdn === '') { + $application->fqdn = null; + } $payload['fqdn'] = $application->fqdn; } if ($application->isDirty('install_command')) { @@ -139,6 +141,11 @@ class Application extends BaseModel if ($application->isDirty('status')) { $payload['last_online_at'] = now(); } + if ($application->isDirty('custom_nginx_configuration')) { + if ($application->custom_nginx_configuration === '') { + $payload['custom_nginx_configuration'] = null; + } + } if (count($payload) > 0) { $application->forceFill($payload); } @@ -172,6 +179,11 @@ class Application extends BaseModel return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name'); } + public static function ownedByCurrentTeam() + { + return Application::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + public function getContainersToStop(bool $previewDeployments = false): array { $containers = $previewDeployments @@ -627,6 +639,14 @@ class Application extends BaseModel ); } + public function customNginxConfiguration(): Attribute + { + return Attribute::make( + set: fn ($value) => base64_encode($value), + get: fn ($value) => base64_decode($value), + ); + } + public function portsExposesArray(): Attribute { return Attribute::make( @@ -857,7 +877,7 @@ class Application extends BaseModel public function isConfigurationChanged(bool $save = false) { - $newConfigHash = $this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect; + $newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration); if ($this->pull_request_id === 0 || $this->pull_request_id === null) { $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); } else { @@ -887,21 +907,7 @@ class Application extends BaseModel public function customRepository() { - preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches); - $port = 22; - if (count($matches) === 1) { - $port = $matches[0]; - $gitHost = str($this->git_repository)->before(':'); - $gitRepo = str($this->git_repository)->after('/'); - $repository = "$gitHost:$gitRepo"; - } else { - $repository = $this->git_repository; - } - - return [ - 'repository' => $repository, - 'port' => $port, - ]; + return convertGitUrl($this->git_repository, $this->deploymentType(), $this->source); } public function generateBaseDir(string $uuid) @@ -934,6 +940,122 @@ class Application extends BaseModel return $git_clone_command; } + public function getGitRemoteStatus(string $deployment_uuid) + { + try { + ['commands' => $lsRemoteCommand] = $this->generateGitLsRemoteCommands(deployment_uuid: $deployment_uuid, exec_in_docker: false); + instant_remote_process([$lsRemoteCommand], $this->destination->server, true); + + return [ + 'is_accessible' => true, + 'error' => null, + ]; + } catch (\RuntimeException $ex) { + return [ + 'is_accessible' => false, + 'error' => $ex->getMessage(), + ]; + } + } + + public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_in_docker = true) + { + $branch = $this->git_branch; + ['repository' => $customRepository, 'port' => $customPort] = $this->customRepository(); + $commands = collect([]); + $base_command = 'git ls-remote'; + + if ($this->deploymentType() === 'source') { + $source_html_url = data_get($this, 'source.html_url'); + $url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL)); + $source_html_url_host = $url['host']; + $source_html_url_scheme = $url['scheme']; + + if ($this->source->getMorphClass() == 'App\Models\GithubApp') { + if ($this->source->is_public) { + $fullRepoUrl = "{$this->source->html_url}/{$customRepository}"; + $base_command = "{$base_command} {$this->source->html_url}/{$customRepository}"; + } else { + $github_access_token = generate_github_installation_token($this->source); + + if ($exec_in_docker) { + $base_command = "{$base_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git"; + $fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git"; + } else { + $base_command = "{$base_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}"; + $fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}"; + } + } + + if ($exec_in_docker) { + $commands->push(executeInDocker($deployment_uuid, $base_command)); + } else { + $commands->push($base_command); + } + + return [ + 'commands' => $commands->implode(' && '), + 'branch' => $branch, + 'fullRepoUrl' => $fullRepoUrl, + ]; + } + } + + if ($this->deploymentType() === 'deploy_key') { + $fullRepoUrl = $customRepository; + $private_key = data_get($this, 'private_key.private_key'); + if (is_null($private_key)) { + throw new RuntimeException('Private key not found. Please add a private key to the application and try again.'); + } + $private_key = base64_encode($private_key); + $base_comamnd = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$base_command} {$customRepository}"; + + if ($exec_in_docker) { + $commands = collect([ + executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'), + executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"), + executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'), + ]); + } else { + $commands = collect([ + 'mkdir -p /root/.ssh', + "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null", + 'chmod 600 /root/.ssh/id_rsa', + ]); + } + + if ($exec_in_docker) { + $commands->push(executeInDocker($deployment_uuid, $base_comamnd)); + } else { + $commands->push($base_comamnd); + } + + return [ + 'commands' => $commands->implode(' && '), + 'branch' => $branch, + 'fullRepoUrl' => $fullRepoUrl, + ]; + } + + if ($this->deploymentType() === 'other') { + $fullRepoUrl = $customRepository; + $base_command = "{$base_command} {$customRepository}"; + $base_command = $this->setGitImportSettings($deployment_uuid, $base_command, public: true); + + if ($exec_in_docker) { + $commands->push(executeInDocker($deployment_uuid, $base_command)); + } else { + $commands->push($base_command); + } + + return [ + 'commands' => $commands->implode(' && '), + 'branch' => $branch, + 'fullRepoUrl' => $fullRepoUrl, + ]; + } + } + public function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null, ?string $commit = null) { $branch = $this->git_branch; @@ -1195,16 +1317,47 @@ class Application extends BaseModel $workdir = rtrim($this->base_directory, '/'); $composeFile = $this->docker_compose_location; $fileList = collect([".$workdir$composeFile"]); - $commands = collect([ - "rm -rf /tmp/{$uuid}", - "mkdir -p /tmp/{$uuid}", - "cd /tmp/{$uuid}", - $cloneCommand, - 'git sparse-checkout init --cone', - "git sparse-checkout set {$fileList->implode(' ')}", - 'git read-tree -mu HEAD', - "cat .$workdir$composeFile", - ]); + $gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid); + if (! $gitRemoteStatus['is_accessible']) { + throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}"); + } + $getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false); + $gitVersion = str($getGitVersion)->explode(' ')->last(); + + if (version_compare($gitVersion, '2.35.1', '<')) { + $fileList = $fileList->map(function ($file) { + $parts = explode('/', trim($file, '.')); + $paths = collect(); + $currentPath = ''; + foreach ($parts as $part) { + $currentPath .= ($currentPath ? '/' : '').$part; + $paths->push($currentPath); + } + + return $paths; + })->flatten()->unique()->values(); + $commands = collect([ + "rm -rf /tmp/{$uuid}", + "mkdir -p /tmp/{$uuid}", + "cd /tmp/{$uuid}", + $cloneCommand, + 'git sparse-checkout init --cone', + "git sparse-checkout set {$fileList->implode(' ')}", + 'git read-tree -mu HEAD', + "cat .$workdir$composeFile", + ]); + } else { + $commands = collect([ + "rm -rf /tmp/{$uuid}", + "mkdir -p /tmp/{$uuid}", + "cd /tmp/{$uuid}", + $cloneCommand, + 'git sparse-checkout init --cone', + "git sparse-checkout set {$fileList->implode(' ')}", + 'git read-tree -mu HEAD', + "cat .$workdir$composeFile", + ]); + } try { $composeFileContent = instant_remote_process($commands, $this->destination->server); } catch (\Exception $e) { diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 17201ea6e..79801987b 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Visus\Cuid2\Cuid2; @@ -18,4 +19,18 @@ abstract class BaseModel extends Model } }); } + + public function name(): Attribute + { + return new Attribute( + get: fn () => sanitize_string($this->getRawOriginal('name')), + ); + } + + public function image(): Attribute + { + return new Attribute( + get: fn () => sanitize_string($this->getRawOriginal('image')), + ); + } } diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 08f23d7ab..96c57e63e 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -71,7 +71,7 @@ class EnvironmentVariable extends Model } } $environment_variable->update([ - 'version' => config('version'), + 'version' => config('constants.coolify.version'), ]); }); static::saving(function (EnvironmentVariable $environmentVariable) { diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php index 065746ede..80015d87f 100644 --- a/app/Models/PrivateKey.php +++ b/app/Models/PrivateKey.php @@ -218,10 +218,12 @@ class PrivateKey extends BaseModel private static function fingerprintExists($fingerprint, $excludeId = null) { - $query = self::where('fingerprint', $fingerprint); + $query = self::query() + ->where('fingerprint', $fingerprint) + ->where('id', '!=', $excludeId); - if (! is_null($excludeId)) { - $query->where('id', '!=', $excludeId); + if (currentTeam()) { + $query->where('team_id', currentTeam()->id); } return $query->exists(); diff --git a/app/Models/Project.php b/app/Models/Project.php index 3a09b0b8f..f27e6c208 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -122,9 +122,18 @@ class Project extends BaseModel return $this->hasManyThrough(StandaloneMariadb::class, Environment::class); } - public function resource_count() + public function isEmpty() { - return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->clickhouses()->count() + $this->services()->count(); + return $this->applications()->count() == 0 && + $this->redis()->count() == 0 && + $this->postgresqls()->count() == 0 && + $this->mysqls()->count() == 0 && + $this->keydbs()->count() == 0 && + $this->dragonflies()->count() == 0 && + $this->clickhouses()->count() == 0 && + $this->mariadbs()->count() == 0 && + $this->mongodbs()->count() == 0 && + $this->services()->count() == 0; } public function databases() diff --git a/app/Models/Server.php b/app/Models/Server.php index e9b6fd929..6dfb0a4a1 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Actions\Proxy\StartProxy; use App\Actions\Server\InstallDocker; use App\Actions\Server\StartSentinel; use App\Enums\ProxyTypes; @@ -10,6 +11,7 @@ use App\Notifications\Server\Reachable; use App\Notifications\Server\Unreachable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; @@ -26,28 +28,28 @@ use Symfony\Component\Yaml\Yaml; description: 'Server model', type: 'object', properties: [ - 'id' => ['type' => 'integer'], - 'uuid' => ['type' => 'string'], - 'name' => ['type' => 'string'], - 'description' => ['type' => 'string'], - 'ip' => ['type' => 'string'], - 'user' => ['type' => 'string'], - 'port' => ['type' => 'integer'], - 'proxy' => ['type' => 'object'], - 'high_disk_usage_notification_sent' => ['type' => 'boolean'], - 'unreachable_notification_sent' => ['type' => 'boolean'], - 'unreachable_count' => ['type' => 'integer'], - 'validation_logs' => ['type' => 'string'], - 'log_drain_notification_sent' => ['type' => 'boolean'], - 'swarm_cluster' => ['type' => 'string'], - 'delete_unused_volumes' => ['type' => 'boolean'], - 'delete_unused_networks' => ['type' => 'boolean'], + 'id' => ['type' => 'integer', 'description' => 'The server ID.'], + 'uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'name' => ['type' => 'string', 'description' => 'The server name.'], + 'description' => ['type' => 'string', 'description' => 'The server description.'], + 'ip' => ['type' => 'string', 'description' => 'The IP address.'], + 'user' => ['type' => 'string', 'description' => 'The user.'], + 'port' => ['type' => 'integer', 'description' => 'The port number.'], + 'proxy' => ['type' => 'object', 'description' => 'The proxy configuration.'], + 'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'description' => 'The proxy type.'], + 'high_disk_usage_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the high disk usage notification has been sent.'], + 'unreachable_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unreachable notification has been sent.'], + 'unreachable_count' => ['type' => 'integer', 'description' => 'The unreachable count for your server.'], + 'validation_logs' => ['type' => 'string', 'description' => 'The validation logs.'], + 'log_drain_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the log drain notification has been sent.'], + 'swarm_cluster' => ['type' => 'string', 'description' => 'The swarm cluster configuration.'], + 'settings' => ['$ref' => '#/components/schemas/ServerSetting'], ] )] class Server extends BaseModel { - use SchemalessAttributesTrait, SoftDeletes; + use HasFactory, SchemalessAttributesTrait, SoftDeletes; public static $batch_counter = 0; @@ -64,7 +66,7 @@ class Server extends BaseModel $server->forceFill($payload); }); static::saved(function ($server) { - if ($server->privateKey->isDirty()) { + if ($server->privateKey?->isDirty()) { refresh_server_connection($server->privateKey); } }); @@ -103,6 +105,14 @@ class Server extends BaseModel ]); } } + if (! isset($server->proxy->redirect_enabled)) { + $server->proxy->redirect_enabled = true; + } + }); + static::retrieved(function ($server) { + if (! isset($server->proxy->redirect_enabled)) { + $server->proxy->redirect_enabled = true; + } }); static::forceDeleting(function ($server) { @@ -182,73 +192,80 @@ class Server extends BaseModel return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server; } - public function setupDefault404Redirect() + public function setupDefaultRedirect() { + $banner = + "# This file is generated by Coolify, do not edit it manually.\n". + "# Disable the default redirect to customize (only if you know what are you doing).\n\n"; $dynamic_conf_path = $this->proxyPath().'/dynamic'; $proxy_type = $this->proxyType(); + $redirect_enabled = $this->proxy->redirect_enabled ?? true; $redirect_url = $this->proxy->redirect_url; - if ($proxy_type === ProxyTypes::TRAEFIK->value) { - $default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml"; - } elseif ($proxy_type === ProxyTypes::CADDY->value) { - $default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy"; - } - if (empty($redirect_url)) { + if (isDev()) { if ($proxy_type === ProxyTypes::CADDY->value) { - $conf = ':80, :443 { -respond 404 -}'; - $conf = - "# This file is automatically generated by Coolify.\n". - "# Do not edit it manually (only if you know what are you doing).\n\n". - $conf; - $base64 = base64_encode($conf); - instant_remote_process([ - "mkdir -p $dynamic_conf_path", - "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", - ], $this); - $this->reloadCaddy(); - - return; + $dynamic_conf_path = '/data/coolify/proxy/caddy/dynamic'; } - instant_remote_process([ - "mkdir -p $dynamic_conf_path", - "rm -f $default_redirect_file", - ], $this); - - return; } if ($proxy_type === ProxyTypes::TRAEFIK->value) { - $dynamic_conf = [ - 'http' => [ - 'routers' => [ - 'catchall' => [ - 'entryPoints' => [ - 0 => 'http', - 1 => 'https', - ], - 'service' => 'noop', - 'rule' => 'HostRegexp(`.+`)', - 'tls' => [ - 'certResolver' => 'letsencrypt', - ], - 'priority' => 1, - 'middlewares' => [ - 0 => 'redirect-regexp', + $default_redirect_file = "$dynamic_conf_path/default_redirect_503.yaml"; + } elseif ($proxy_type === ProxyTypes::CADDY->value) { + $default_redirect_file = "$dynamic_conf_path/default_redirect_503.caddy"; + } + + instant_remote_process([ + "mkdir -p $dynamic_conf_path", + "rm -f $dynamic_conf_path/default_redirect_404.yaml", + "rm -f $dynamic_conf_path/default_redirect_404.caddy", + ], $this); + + if ($redirect_enabled === false) { + instant_remote_process(["rm -f $default_redirect_file"], $this); + } else { + if ($proxy_type === ProxyTypes::CADDY->value) { + if (filled($redirect_url)) { + $conf = ":80, :443 { + redir $redirect_url +}"; + } else { + $conf = ':80, :443 { + respond 503 +}'; + } + } elseif ($proxy_type === ProxyTypes::TRAEFIK->value) { + $dynamic_conf = [ + 'http' => [ + 'routers' => [ + 'catchall' => [ + 'entryPoints' => [ + 0 => 'http', + 1 => 'https', + ], + 'service' => 'noop', + 'rule' => 'PathPrefix(`/`)', + 'tls' => [ + 'certResolver' => 'letsencrypt', + ], + 'priority' => -1000, ], ], - ], - 'services' => [ - 'noop' => [ - 'loadBalancer' => [ - 'servers' => [ - 0 => [ - 'url' => '', - ], + 'services' => [ + 'noop' => [ + 'loadBalancer' => [ + 'servers' => [], ], ], ], ], - 'middlewares' => [ + ]; + if (filled($redirect_url)) { + $dynamic_conf['http']['routers']['catchall']['middlewares'] = [ + 0 => 'redirect-regexp', + ]; + + $dynamic_conf['http']['services']['noop']['loadBalancer']['servers'][0] = [ + 'url' => '', + ]; + $dynamic_conf['http']['middlewares'] = [ 'redirect-regexp' => [ 'redirectRegex' => [ 'regex' => '(.*)', @@ -256,32 +273,17 @@ respond 404 'permanent' => false, ], ], - ], - ], - ]; - $conf = Yaml::dump($dynamic_conf, 12, 2); - $conf = - "# This file is automatically generated by Coolify.\n". - "# Do not edit it manually (only if you know what are you doing).\n\n". - $conf; - - $base64 = base64_encode($conf); - } elseif ($proxy_type === ProxyTypes::CADDY->value) { - $conf = ":80, :443 { - redir $redirect_url -}"; - $conf = - "# This file is automatically generated by Coolify.\n". - "# Do not edit it manually (only if you know what are you doing).\n\n". - $conf; + ]; + } + $conf = Yaml::dump($dynamic_conf, 12, 2); + } + $conf = $banner.$conf; $base64 = base64_encode($conf); + instant_remote_process([ + "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", + ], $this); } - instant_remote_process([ - "mkdir -p $dynamic_conf_path", - "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null", - ], $this); - if ($proxy_type === 'CADDY') { $this->reloadCaddy(); } @@ -462,7 +464,7 @@ $schema://$host { public function proxyPath() { - $base_path = config('coolify.base_config_path'); + $base_path = config('constants.coolify.base_config_path'); $proxyType = $this->proxyType(); $proxy_path = "$base_path/proxy"; // TODO: should use /traefik for already exisiting configurations? @@ -609,7 +611,9 @@ $schema://$host { } $memory = json_decode($memory, true); $parsedCollection = collect($memory)->map(function ($metric) { - return [(int) $metric['time'], (float) $metric['usedPercent']]; + $usedPercent = $metric['usedPercent'] ?? 0.0; + + return [(int) $metric['time'], (float) $usedPercent]; }); return $parsedCollection->toArray(); @@ -812,7 +816,7 @@ $schema://$host { { return Attribute::make( get: function ($value) { - return preg_replace('/[^0-9]/', '', $value); + return (int) preg_replace('/[^0-9]/', '', $value); } ); } @@ -974,10 +978,10 @@ $schema://$host { public function serverStatus(): bool { - if ($this->isFunctional() === false) { + if ($this->status() === false) { return false; } - if ($this->status() === false) { + if ($this->isFunctional() === false) { return false; } @@ -986,10 +990,7 @@ $schema://$host { public function status(): bool { - if ($this->isFunctional() === false) { - return false; - } - ['uptime' => $uptime] = $this->validateConnection(false); + ['uptime' => $uptime] = $this->validateConnection(); if ($uptime === false) { foreach ($this->applications() as $application) { $application->status = 'exited'; @@ -1041,7 +1042,7 @@ $schema://$host { $this->unreachable_notification_sent = false; $this->save(); $this->refresh(); - $this->team->notify(new Reachable($this)); + // $this->team->notify(new Reachable($this)); } public function sendUnreachableNotification() @@ -1049,21 +1050,17 @@ $schema://$host { $this->unreachable_notification_sent = true; $this->save(); $this->refresh(); - $this->team->notify(new Unreachable($this)); + // $this->team->notify(new Unreachable($this)); } - public function validateConnection(bool $isManualCheck = true, bool $justCheckingNewKey = false) + public function validateConnection(bool $justCheckingNewKey = false) { - config()->set('constants.ssh.mux_enabled', ! $isManualCheck); + config()->set('constants.ssh.mux_enabled', false); if ($this->skipServer()) { return ['uptime' => false, 'error' => 'Server skipped.']; } try { - // Make sure the private key is stored - if ($this->privateKey) { - $this->privateKey->storeInFileSystem(); - } instant_remote_process(['ls /'], $this); if ($this->settings->is_reachable === false) { $this->settings->is_reachable = true; @@ -1232,7 +1229,7 @@ $schema://$host { return str($this->ip)->contains(':'); } - public function restartSentinel(bool $async = true): void + public function restartSentinel(bool $async = true) { try { if ($async) { @@ -1241,7 +1238,7 @@ $schema://$host { StartSentinel::run($this, true); } } catch (\Throwable $e) { - loggy('Error restarting Sentinel: '.$e->getMessage()); + return handleError($e); } } @@ -1254,4 +1251,25 @@ $schema://$host { { return instant_remote_process(['docker restart '.$containerName], $this, false); } + + public function changeProxy(string $proxyType, bool $async = true) + { + $validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) { + return str($proxyType->value)->lower(); + }); + if ($validProxyTypes->contains(str($proxyType)->lower())) { + $this->proxy->set('type', str($proxyType)->upper()); + $this->proxy->set('status', 'exited'); + $this->save(); + if ($this->proxySet()) { + if ($async) { + StartProxy::dispatch($this); + } else { + StartProxy::run($this); + } + } + } else { + throw new \Exception('Invalid proxy type.'); + } + } } diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index bca16536e..e078372e2 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Log; use OpenApi\Attributes as OA; #[OA\Schema( @@ -44,6 +45,8 @@ use OpenApi\Attributes as OA; 'wildcard_domain' => ['type' => 'string'], 'created_at' => ['type' => 'string'], 'updated_at' => ['type' => 'string'], + 'delete_unused_volumes' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused volumes should be deleted.'], + 'delete_unused_networks' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused networks should be deleted.'], ] )] class ServerSetting extends Model @@ -63,13 +66,13 @@ class ServerSetting extends Model static::creating(function ($setting) { try { if (str($setting->sentinel_token)->isEmpty()) { - $setting->generateSentinelToken(save: false); + $setting->generateSentinelToken(save: false, ignoreEvent: true); } if (str($setting->sentinel_custom_url)->isEmpty()) { - $setting->generateSentinelUrl(save: false); + $setting->generateSentinelUrl(save: false, ignoreEvent: true); } } catch (\Throwable $e) { - loggy('Error creating server setting: '.$e->getMessage()); + Log::error('Error creating server setting: '.$e->getMessage()); } }); static::updated(function ($settings) { @@ -88,7 +91,7 @@ class ServerSetting extends Model }); } - public function generateSentinelToken(bool $save = true) + public function generateSentinelToken(bool $save = true, bool $ignoreEvent = false) { $data = [ 'server_uuid' => $this->server->uuid, @@ -97,13 +100,17 @@ class ServerSetting extends Model $encrypted = encrypt($token); $this->sentinel_token = $encrypted; if ($save) { - $this->save(); + if ($ignoreEvent) { + $this->saveQuietly(); + } else { + $this->save(); + } } return $token; } - public function generateSentinelUrl(bool $save = true) + public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false) { $domain = null; $settings = InstanceSettings::get(); @@ -118,7 +125,11 @@ class ServerSetting extends Model } $this->sentinel_custom_url = $domain; if ($save) { - $this->save(); + if ($ignoreEvent) { + $this->saveQuietly(); + } else { + $this->save(); + } } return $domain; diff --git a/app/Models/Service.php b/app/Models/Service.php index f88a23641..6d3d2024b 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -133,6 +133,11 @@ class Service extends BaseModel return $this->morphToMany(Tag::class, 'taggable'); } + public static function ownedByCurrentTeam() + { + return Service::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + public function getContainersToStop(): array { $containersToStop = []; @@ -1166,7 +1171,7 @@ class Service extends BaseModel $services = get_service_templates(); $service = data_get($services, str($this->name)->beforeLast('-')->value, []); - return data_get($service, 'documentation', config('constants.docs.base_url')); + return data_get($service, 'documentation', config('constants.urls.docs')); } public function applications() diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index 305913068..5cafc9042 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -37,6 +37,11 @@ class ServiceApplication extends BaseModel return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); } + public static function ownedByCurrentTeam() + { + return ServiceApplication::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + public function isRunning() { return str($this->status)->contains('running'); diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 6641509dd..5fdd52637 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -24,6 +24,16 @@ class ServiceDatabase extends BaseModel }); } + public static function ownedByCurrentTeamAPI(int $teamId) + { + return ServiceDatabase::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); + } + + public static function ownedByCurrentTeam() + { + return ServiceDatabase::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + public function restart() { $container_id = $this->name.'-'.$this->service->uuid; diff --git a/app/Models/Team.php b/app/Models/Team.php index 5b4a80cb1..ecf662787 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Notifications\Channels\SendsDiscord; use App\Notifications\Channels\SendsEmail; +use App\Notifications\Channels\SendsSlack; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; @@ -70,7 +71,7 @@ use OpenApi\Attributes as OA; ), ] )] -class Team extends Model implements SendsDiscord, SendsEmail +class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack { use Notifiable; @@ -127,6 +128,11 @@ class Team extends Model implements SendsDiscord, SendsEmail ]; } + public function routeNotificationForSlack() + { + return data_get($this, 'slack_webhook_url', null); + } + public function getRecepients($notification) { $recipients = data_get($notification, 'emails', null); @@ -165,14 +171,14 @@ class Team extends Model implements SendsDiscord, SendsEmail return 0; } - return data_get($team, 'limits.serverLimit', 0); + return data_get($team, 'limits', 0); } public function limits(): Attribute { return Attribute::make( get: function () { - if (config('coolify.self_hosted') || $this->id === 0) { + if (config('constants.coolify.self_hosted') || $this->id === 0) { $subscription = 'self-hosted'; } else { $subscription = data_get($this, 'subscription'); @@ -187,9 +193,8 @@ class Team extends Model implements SendsDiscord, SendsEmail } else { $serverLimit = config('constants.limits.server')[strtolower($subscription)]; } - $sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)]; - return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled]; + return $serverLimit ?? 2; } ); @@ -258,8 +263,15 @@ class Team extends Model implements SendsDiscord, SendsEmail return $this->hasMany(S3Storage::class)->where('is_usable', true); } - public function trialEnded() + public function subscriptionEnded() { + $this->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, + ]); foreach ($this->servers as $server) { $server->settings()->update([ 'is_usable' => false, @@ -268,16 +280,6 @@ class Team extends Model implements SendsDiscord, SendsEmail } } - public function trialEndedButSubscribed() - { - foreach ($this->servers as $server) { - $server->settings()->update([ - 'is_usable' => true, - 'is_reachable' => true, - ]); - } - } - public function isAnyNotificationEnabled() { if (isCloud()) { diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php index 0f298a829..bc1a90d58 100644 --- a/app/Models/TeamInvitation.php +++ b/app/Models/TeamInvitation.php @@ -28,8 +28,8 @@ class TeamInvitation extends Model public function isValid() { $createdAt = $this->created_at; - $diff = $createdAt->diffInMinutes(now()); - if ($diff <= config('constants.invitation.link.expiration')) { + $diff = $createdAt->diffInDays(now()); + if ($diff <= config('constants.invitation.link.expiration_days')) { return true; } else { $this->delete(); diff --git a/app/Models/User.php b/app/Models/User.php index ecc4ef6b6..25fb33d66 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,6 +10,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\URL; @@ -158,7 +159,7 @@ class User extends Authenticatable implements SendsEmail public function isAdminFromSession() { - if (auth()->user()->id === 0) { + if (Auth::id() === 0) { return true; } $teams = $this->teams()->get(); @@ -178,9 +179,9 @@ class User extends Authenticatable implements SendsEmail public function isInstanceAdmin() { - $found_root_team = auth()->user()->teams->filter(function ($team) { + $found_root_team = Auth::user()->teams->filter(function ($team) { if ($team->id == 0) { - if (! auth()->user()->isAdmin()) { + if (! Auth::user()->isAdmin()) { return false; } @@ -195,9 +196,9 @@ class User extends Authenticatable implements SendsEmail public function currentTeam() { - return Cache::remember('team:'.auth()->user()->id, 3600, function () { - if (is_null(data_get(session('currentTeam'), 'id')) && auth()->user()->teams->count() > 0) { - return auth()->user()->teams[0]; + return Cache::remember('team:'.Auth::id(), 3600, function () { + if (is_null(data_get(session('currentTeam'), 'id')) && Auth::user()->teams->count() > 0) { + return Auth::user()->teams[0]; } return Team::find(session('currentTeam')->id); @@ -206,7 +207,7 @@ class User extends Authenticatable implements SendsEmail public function otherTeams() { - return auth()->user()->teams->filter(function ($team) { + return Auth::user()->teams->filter(function ($team) { return $team->id != currentTeam()->id; }); } @@ -216,7 +217,7 @@ class User extends Authenticatable implements SendsEmail if (data_get($this, 'pivot')) { return $this->pivot->role; } - $user = auth()->user()->teams->where('id', currentTeam()->id)->first(); + $user = Auth::user()->teams->where('id', currentTeam()->id)->first(); return data_get($user, 'pivot.role'); } diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index 242980e00..7648aaac2 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -4,18 +4,13 @@ namespace App\Notifications\Application; use App\Models\Application; use App\Models\ApplicationPreview; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class DeploymentFailed extends Notification implements ShouldQueue +class DeploymentFailed extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - public Application $application; public ?ApplicationPreview $preview = null; @@ -34,6 +29,7 @@ class DeploymentFailed extends Notification implements ShouldQueue public function __construct(Application $application, string $deployment_uuid, ?ApplicationPreview $preview = null) { + $this->onQueue('high'); $this->application = $application; $this->deployment_uuid = $deployment_uuid; $this->preview = $preview; @@ -133,4 +129,31 @@ class DeploymentFailed extends Notification implements ShouldQueue ], ]; } + + public function toSlack(): SlackMessage + { + if ($this->preview) { + $title = "Pull request #{$this->preview->pull_request_id} deployment failed"; + $description = "Pull request deployment failed for {$this->application_name}"; + if ($this->preview->fqdn) { + $description .= "\nPreview URL: {$this->preview->fqdn}"; + } + } else { + $title = 'Deployment failed'; + $description = "Deployment failed for {$this->application_name}"; + if ($this->fqdn) { + $description .= "\nApplication URL: {$this->fqdn}"; + } + } + + $description .= "\n\n**Project:** ".data_get($this->application, 'environment.project.name'); + $description .= "\n**Environment:** {$this->environment_name}"; + $description .= "\n**Deployment Logs:** {$this->deployment_url}"; + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 946a622ca..79ae19f66 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -4,18 +4,13 @@ namespace App\Notifications\Application; use App\Models\Application; use App\Models\ApplicationPreview; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class DeploymentSuccess extends Notification implements ShouldQueue +class DeploymentSuccess extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - public Application $application; public ?ApplicationPreview $preview = null; @@ -34,6 +29,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue public function __construct(Application $application, string $deployment_uuid, ?ApplicationPreview $preview = null) { + $this->onQueue('high'); $this->application = $application; $this->deployment_uuid = $deployment_uuid; $this->preview = $preview; @@ -148,4 +144,31 @@ class DeploymentSuccess extends Notification implements ShouldQueue ], ]; } + + public function toSlack(): SlackMessage + { + if ($this->preview) { + $title = "Pull request #{$this->preview->pull_request_id} successfully deployed"; + $description = "New version successfully deployed for {$this->application_name}"; + if ($this->preview->fqdn) { + $description .= "\nPreview URL: {$this->preview->fqdn}"; + } + } else { + $title = 'New version successfully deployed'; + $description = "New version successfully deployed for {$this->application_name}"; + if ($this->fqdn) { + $description .= "\nApplication URL: {$this->fqdn}"; + } + } + + $description .= "\n\n**Project:** ".data_get($this->application, 'environment.project.name'); + $description .= "\n**Environment:** {$this->environment_name}"; + $description .= "\n**Deployment Logs:** {$this->deployment_url}"; + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::successColor() + ); + } } diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php index 852c6b526..2167e9f13 100644 --- a/app/Notifications/Application/StatusChanged.php +++ b/app/Notifications/Application/StatusChanged.php @@ -3,18 +3,13 @@ namespace App\Notifications\Application; use App\Models\Application; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class StatusChanged extends Notification implements ShouldQueue +class StatusChanged extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - public string $resource_name; public string $project_uuid; @@ -27,6 +22,7 @@ class StatusChanged extends Notification implements ShouldQueue public function __construct(public Application $resource) { + $this->onQueue('high'); $this->resource_name = data_get($resource, 'name'); $this->project_uuid = data_get($resource, 'environment.project.uuid'); $this->environment_name = data_get($resource, 'environment.name'); @@ -80,4 +76,20 @@ class StatusChanged extends Notification implements ShouldQueue ], ]; } + + public function toSlack(): SlackMessage + { + $title = 'Application stopped'; + $description = "{$this->resource_name} has been stopped"; + + $description .= "\n\n**Project:** ".data_get($this->resource, 'environment.project.name'); + $description .= "\n**Environment:** {$this->environment_name}"; + $description .= "\n**Application URL:** {$this->resource_url}"; + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Channels/DiscordChannel.php b/app/Notifications/Channels/DiscordChannel.php index 3a33d8902..df7040f8f 100644 --- a/app/Notifications/Channels/DiscordChannel.php +++ b/app/Notifications/Channels/DiscordChannel.php @@ -17,6 +17,6 @@ class DiscordChannel if (! $webhookUrl) { return; } - dispatch(new SendMessageToDiscordJob($message, $webhookUrl)); + SendMessageToDiscordJob::dispatch($message, $webhookUrl); } } diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index af9af978d..5394f6106 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -66,11 +66,12 @@ class EmailChannel 'transport' => 'smtp', 'host' => data_get($notifiable, 'smtp_host'), 'port' => data_get($notifiable, 'smtp_port'), - 'encryption' => data_get($notifiable, 'smtp_encryption'), + 'encryption' => data_get($notifiable, 'smtp_encryption') === 'none' ? null : data_get($notifiable, 'smtp_encryption'), 'username' => data_get($notifiable, 'smtp_username'), 'password' => data_get($notifiable, 'smtp_password'), 'timeout' => data_get($notifiable, 'smtp_timeout'), 'local_domain' => null, + 'auto_tls' => data_get($notifiable, 'smtp_encryption') === 'none' ? '0' : '', ]); } } diff --git a/app/Notifications/Channels/SendsSlack.php b/app/Notifications/Channels/SendsSlack.php new file mode 100644 index 000000000..ab2dd6f11 --- /dev/null +++ b/app/Notifications/Channels/SendsSlack.php @@ -0,0 +1,8 @@ +toSlack(); + $webhookUrl = $notifiable->routeNotificationForSlack(); + if (! $webhookUrl) { + return; + } + SendMessageToSlackJob::dispatch($message, $webhookUrl); + } +} diff --git a/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php index 4b1fa49dd..958c46c21 100644 --- a/app/Notifications/Channels/TelegramChannel.php +++ b/app/Notifications/Channels/TelegramChannel.php @@ -41,6 +41,6 @@ class TelegramChannel if (! $telegramToken || ! $chatId || ! $message) { return; } - dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId)); + SendMessageToTelegramJob::dispatch($message, $buttons, $telegramToken, $chatId, $topicId); } } diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php index 182a1f5fc..f9becd0e8 100644 --- a/app/Notifications/Container/ContainerRestarted.php +++ b/app/Notifications/Container/ContainerRestarted.php @@ -3,19 +3,17 @@ namespace App\Notifications\Container; use App\Models\Server; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class ContainerRestarted extends Notification implements ShouldQueue +class ContainerRestarted extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - - public function __construct(public string $name, public Server $server, public ?string $url = null) {} + public function __construct(public string $name, public Server $server, public ?string $url = null) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { @@ -69,4 +67,20 @@ class ContainerRestarted extends Notification implements ShouldQueue return $payload; } + + public function toSlack(): SlackMessage + { + $title = 'Resource restarted'; + $description = "A resource ({$this->name}) has been restarted automatically on {$this->server->name}"; + + if ($this->url) { + $description .= "\n**Resource URL:** {$this->url}"; + } + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::warningColor() + ); + } } diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php index 33a55c65a..eae2cf552 100644 --- a/app/Notifications/Container/ContainerStopped.php +++ b/app/Notifications/Container/ContainerStopped.php @@ -3,19 +3,17 @@ namespace App\Notifications\Container; use App\Models\Server; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class ContainerStopped extends Notification implements ShouldQueue +class ContainerStopped extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - - public function __construct(public string $name, public Server $server, public ?string $url = null) {} + public function __construct(public string $name, public Server $server, public ?string $url = null) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { @@ -69,4 +67,20 @@ class ContainerStopped extends Notification implements ShouldQueue return $payload; } + + public function toSlack(): SlackMessage + { + $title = 'Resource stopped'; + $description = "A resource ({$this->name}) has been stopped unexpectedly on {$this->server->name}"; + + if ($this->url) { + $description .= "\n**Resource URL:** {$this->url}"; + } + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/CustomEmailNotification.php b/app/Notifications/CustomEmailNotification.php new file mode 100644 index 000000000..c3c89b30f --- /dev/null +++ b/app/Notifications/CustomEmailNotification.php @@ -0,0 +1,18 @@ +onQueue('high'); $this->name = $database->name; $this->frequency = $backup->frequency; } @@ -69,4 +63,19 @@ class BackupFailed extends Notification implements ShouldQueue 'message' => $message, ]; } + + public function toSlack(): SlackMessage + { + $title = 'Database backup failed'; + $description = "Database backup for {$this->name} (db:{$this->database_name}) has FAILED."; + + $description .= "\n\n**Frequency:** {$this->frequency}"; + $description .= "\n\n**Error Output:**\n{$this->output}"; + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index 5128c8ed6..71866f9fc 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -3,26 +3,21 @@ namespace App\Notifications\Database; use App\Models\ScheduledDatabaseBackup; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class BackupSuccess extends Notification implements ShouldQueue +class BackupSuccess extends CustomEmailNotification { - use Queueable; - - public $backoff = 10; - - public $tries = 3; - public string $name; public string $frequency; public function __construct(ScheduledDatabaseBackup $backup, public $database, public $database_name) { + $this->onQueue('high'); + $this->name = $database->name; $this->frequency = $backup->frequency; } @@ -66,4 +61,18 @@ class BackupSuccess extends Notification implements ShouldQueue 'message' => $message, ]; } + + public function toSlack(): SlackMessage + { + $title = 'Database backup successful'; + $description = "Database backup for {$this->name} (db:{$this->database_name}) was successful."; + + $description .= "\n\n**Frequency:** {$this->frequency}"; + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::successColor() + ); + } } diff --git a/app/Notifications/Dto/DiscordMessage.php b/app/Notifications/Dto/DiscordMessage.php index 856753dca..278bfd1b6 100644 --- a/app/Notifications/Dto/DiscordMessage.php +++ b/app/Notifications/Dto/DiscordMessage.php @@ -46,7 +46,7 @@ class DiscordMessage public function toPayload(): array { - $footerText = 'Coolify v'.config('version'); + $footerText = 'Coolify v'.config('constants.coolify.version'); if (isCloud()) { $footerText = 'Coolify Cloud'; } diff --git a/app/Notifications/Dto/SlackMessage.php b/app/Notifications/Dto/SlackMessage.php new file mode 100644 index 000000000..879bf6547 --- /dev/null +++ b/app/Notifications/Dto/SlackMessage.php @@ -0,0 +1,32 @@ +onQueue('high'); + } public function via(object $notifiable): array { $channels = []; $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; @@ -29,6 +35,9 @@ class GeneralNotification extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -48,4 +57,13 @@ class GeneralNotification extends Notification implements ShouldQueue 'message' => $this->message, ]; } + + public function toSlack(): SlackMessage + { + return new SlackMessage( + title: 'Coolify: General Notification', + description: $this->message, + color: SlackMessage::infoColor(), + ); + } } diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php index c3501a8eb..753da7ff0 100644 --- a/app/Notifications/ScheduledTask/TaskFailed.php +++ b/app/Notifications/ScheduledTask/TaskFailed.php @@ -3,24 +3,18 @@ namespace App\Notifications\ScheduledTask; use App\Models\ScheduledTask; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class TaskFailed extends Notification implements ShouldQueue +class TaskFailed extends CustomEmailNotification { - use Queueable; - - public $backoff = 10; - - public $tries = 2; - public ?string $url = null; public function __construct(public ScheduledTask $task, public string $output) { + $this->onQueue('high'); if ($task->application) { $this->url = $task->application->failedTaskLink($task->uuid); } elseif ($task->service) { @@ -75,4 +69,24 @@ class TaskFailed extends Notification implements ShouldQueue 'message' => $message, ]; } + + public function toSlack(): SlackMessage + { + $title = 'Scheduled task failed'; + $description = "Scheduled task ({$this->task->name}) failed."; + + if ($this->output) { + $description .= "\n\n**Error Output:**\n{$this->output}"; + } + + if ($this->url) { + $description .= "\n\n**Task URL:** {$this->url}"; + } + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Server/DockerCleanup.php b/app/Notifications/Server/DockerCleanup.php index 7ea1b84c2..46b730c7b 100644 --- a/app/Notifications/Server/DockerCleanup.php +++ b/app/Notifications/Server/DockerCleanup.php @@ -5,18 +5,16 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Notifications\Notification; +use App\Notifications\Dto\SlackMessage; -class DockerCleanup extends Notification implements ShouldQueue +class DockerCleanup extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - - public function __construct(public Server $server, public string $message) {} + public function __construct(public Server $server, public string $message) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { @@ -24,7 +22,7 @@ class DockerCleanup extends Notification implements ShouldQueue // $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; } @@ -34,6 +32,9 @@ class DockerCleanup extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -65,4 +66,13 @@ class DockerCleanup extends Notification implements ShouldQueue 'message' => "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}", ]; } + + public function toSlack(): SlackMessage + { + return new SlackMessage( + title: 'Server cleanup job done', + description: "Server '{$this->server->name}' cleanup job done!\n\n{$this->message}", + color: SlackMessage::successColor() + ); + } } diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php index a26c803ee..143917dde 100644 --- a/app/Notifications/Server/ForceDisabled.php +++ b/app/Notifications/Server/ForceDisabled.php @@ -5,20 +5,19 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; +use App\Notifications\Channels\SlackChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class ForceDisabled extends Notification implements ShouldQueue +class ForceDisabled extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - - public function __construct(public Server $server) {} + public function __construct(public Server $server) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { @@ -26,7 +25,7 @@ class ForceDisabled extends Notification implements ShouldQueue $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; } @@ -36,6 +35,9 @@ class ForceDisabled extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -70,4 +72,18 @@ class ForceDisabled extends Notification implements ShouldQueue 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).", ]; } + + public function toSlack(): SlackMessage + { + $title = 'Server disabled'; + $description = "Server ({$this->server->name}) disabled because it is not paid!\n"; + $description .= "All automations and integrations are stopped.\n\n"; + $description .= 'Please update your subscription to enable the server again: https://app.coolify.io/subscriptions'; + + return new SlackMessage( + title: $title, + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php index 65b65a10c..3b83882d8 100644 --- a/app/Notifications/Server/ForceEnabled.php +++ b/app/Notifications/Server/ForceEnabled.php @@ -5,20 +5,19 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; +use App\Notifications\Channels\SlackChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class ForceEnabled extends Notification implements ShouldQueue +class ForceEnabled extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - - public function __construct(public Server $server) {} + public function __construct(public Server $server) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { @@ -26,7 +25,7 @@ class ForceEnabled extends Notification implements ShouldQueue $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; } @@ -36,6 +35,9 @@ class ForceEnabled extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -66,4 +68,13 @@ class ForceEnabled extends Notification implements ShouldQueue 'message' => "Coolify: Server ({$this->server->name}) enabled again!", ]; } + + public function toSlack(): SlackMessage + { + return new SlackMessage( + title: 'Server enabled', + description: "Server '{$this->server->name}' enabled again!", + color: SlackMessage::successColor() + ); + } } diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php index e373abc03..baff49508 100644 --- a/app/Notifications/Server/HighDiskUsage.php +++ b/app/Notifications/Server/HighDiskUsage.php @@ -3,19 +3,17 @@ namespace App\Notifications\Server; use App\Models\Server; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class HighDiskUsage extends Notification implements ShouldQueue +class HighDiskUsage extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - - public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold) {} + public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { @@ -58,4 +56,22 @@ class HighDiskUsage extends Notification implements ShouldQueue 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->server_disk_usage_notification_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.", ]; } + + public function toSlack(): SlackMessage + { + $description = "Server '{$this->server->name}' high disk usage detected!\n"; + $description .= "Disk usage: {$this->disk_usage}%\n"; + $description .= "Threshold: {$this->server_disk_usage_notification_threshold}%\n\n"; + $description .= "Please cleanup your disk to prevent data-loss.\n"; + $description .= "Tips for cleanup: https://coolify.io/docs/knowledge-base/server/automated-cleanup\n"; + $description .= "Change settings:\n"; + $description .= '- Threshold: '.base_url().'/server/'.$this->server->uuid."#advanced\n"; + $description .= '- Notifications: '.base_url().'/notifications/discord'; + + return new SlackMessage( + title: 'High disk usage detected', + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Server/Reachable.php b/app/Notifications/Server/Reachable.php index 9b54501d9..62ece34e8 100644 --- a/app/Notifications/Server/Reachable.php +++ b/app/Notifications/Server/Reachable.php @@ -5,23 +5,20 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; +use App\Notifications\Channels\SlackChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class Reachable extends Notification implements ShouldQueue +class Reachable extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - protected bool $isRateLimited = false; public function __construct(public Server $server) { + $this->onQueue('high'); $this->isRateLimited = isEmailRateLimited( limiterKey: 'server-reachable:'.$this->server->id, ); @@ -37,7 +34,7 @@ class Reachable extends Notification implements ShouldQueue $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; } @@ -47,6 +44,9 @@ class Reachable extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -77,4 +77,13 @@ class Reachable extends Notification implements ShouldQueue 'message' => "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!", ]; } + + public function toSlack(): SlackMessage + { + return new SlackMessage( + title: 'Server revived', + description: "Server '{$this->server->name}' revived.\nAll automations & integrations are turned on again!", + color: SlackMessage::successColor() + ); + } } diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index 5bc568e82..2a90d7552 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -5,23 +5,20 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; +use App\Notifications\Channels\SlackChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class Unreachable extends Notification implements ShouldQueue +class Unreachable extends CustomEmailNotification { - use Queueable; - - public $tries = 1; - protected bool $isRateLimited = false; public function __construct(public Server $server) { + $this->onQueue('high'); $this->isRateLimited = isEmailRateLimited( limiterKey: 'server-unreachable:'.$this->server->id, ); @@ -37,6 +34,7 @@ class Unreachable extends Notification implements ShouldQueue $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); if ($isDiscordEnabled) { $channels[] = DiscordChannel::class; @@ -47,6 +45,9 @@ class Unreachable extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -81,4 +82,17 @@ class Unreachable extends Notification implements ShouldQueue 'message' => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.", ]; } + + public function toSlack(): SlackMessage + { + $description = "Your server '{$this->server->name}' is unreachable.\n"; + $description .= "All automations & integrations are turned off!\n\n"; + $description .= '*IMPORTANT:* We automatically try to revive your server and turn on all automations & integrations.'; + + return new SlackMessage( + title: 'Server unreachable', + description: $description, + color: SlackMessage::errorColor() + ); + } } diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index a43b1e153..03f6c3296 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -3,6 +3,7 @@ namespace App\Notifications; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\SlackMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -15,7 +16,10 @@ class Test extends Notification implements ShouldQueue public $tries = 5; - public function __construct(public ?string $emails = null) {} + public function __construct(public ?string $emails = null) + { + $this->onQueue('high'); + } public function via(object $notifiable): array { @@ -64,4 +68,12 @@ class Test extends Notification implements ShouldQueue ], ]; } + + public function toSlack(): SlackMessage + { + return new SlackMessage( + title: 'Test Slack Notification', + description: 'This is a test Slack notification from Coolify.' + ); + } } diff --git a/app/Notifications/TransactionalEmails/InvitationLink.php b/app/Notifications/TransactionalEmails/InvitationLink.php index 6da2a6fcc..30ace99dc 100644 --- a/app/Notifications/TransactionalEmails/InvitationLink.php +++ b/app/Notifications/TransactionalEmails/InvitationLink.php @@ -6,23 +6,20 @@ use App\Models\Team; use App\Models\TeamInvitation; use App\Models\User; use App\Notifications\Channels\TransactionalEmailChannel; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\CustomEmailNotification; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class InvitationLink extends Notification implements ShouldQueue +class InvitationLink extends CustomEmailNotification { - use Queueable; - - public $tries = 5; - public function via(): array { return [TransactionalEmailChannel::class]; } - public function __construct(public User $user) {} + public function __construct(public User $user) + { + $this->onQueue('high'); + } public function toMail(): MailMessage { diff --git a/app/Notifications/TransactionalEmails/Test.php b/app/Notifications/TransactionalEmails/Test.php index 64883a58e..eeb32a254 100644 --- a/app/Notifications/TransactionalEmails/Test.php +++ b/app/Notifications/TransactionalEmails/Test.php @@ -3,18 +3,15 @@ namespace App\Notifications\TransactionalEmails; use App\Notifications\Channels\EmailChannel; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use App\Notifications\CustomEmailNotification; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Notifications\Notification; -class Test extends Notification implements ShouldQueue +class Test extends CustomEmailNotification { - use Queueable; - - public $tries = 5; - - public function __construct(public string $emails) {} + public function __construct(public string $emails) + { + $this->onQueue('high'); + } public function via(): array { diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index e8784bab3..bbbf48345 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -50,7 +50,7 @@ class FortifyServiceProvider extends ServiceProvider if (! $settings->is_registration_enabled) { return redirect()->route('login'); } - if (config('coolify.waitlist')) { + if (config('constants.waitlist.enabled')) { return redirect()->route('waitlist.index'); } else { return view('auth.register', [ diff --git a/app/View/Components/Forms/Checkbox.php b/app/View/Components/Forms/Checkbox.php index 8abe96c1b..e46598e8e 100644 --- a/app/View/Components/Forms/Checkbox.php +++ b/app/View/Components/Forms/Checkbox.php @@ -18,11 +18,14 @@ class Checkbox extends Component public ?string $domValue = null, public ?string $label = null, public ?string $helper = null, + public string|bool|null $checked = false, public string|bool $instantSave = false, public bool $disabled = false, public string $defaultClass = 'dark:border-neutral-700 text-coolgray-400 focus:ring-warning dark:bg-coolgray-100 rounded cursor-pointer dark:disabled:bg-base dark:disabled:cursor-not-allowed', ) { - // + if ($this->disabled) { + $this->defaultClass .= ' opacity-40'; + } } /** diff --git a/bootstrap/getVersion.php b/bootstrap/getVersion.php index a8329a319..8fa27a332 100644 --- a/bootstrap/getVersion.php +++ b/bootstrap/getVersion.php @@ -1,4 +1,10 @@ id, - ))->onQueue('high'); + ); } elseif (next_queuable($server_id, $application_id)) { - dispatch(new ApplicationDeploymentJob( + ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $deployment->id, - ))->onQueue('high'); + ); } } function force_start_deployment(ApplicationDeploymentQueue $deployment) @@ -59,9 +59,9 @@ function force_start_deployment(ApplicationDeploymentQueue $deployment) 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, ]); - dispatch(new ApplicationDeploymentJob( + ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $deployment->id, - ))->onQueue('high'); + ); } function queue_next_deployment(Application $application) { @@ -72,9 +72,9 @@ function queue_next_deployment(Application $application) 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, ]); - dispatch(new ApplicationDeploymentJob( + ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $next_found->id, - ))->onQueue('high'); + ); } } @@ -91,7 +91,7 @@ function next_queuable(string $server_id, string $application_id): bool $server = Server::find($server_id); $concurrent_builds = $server->settings->concurrent_builds; - ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}")->green(); + // ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}")->green(); if ($deployments->count() > $concurrent_builds) { return false; @@ -113,9 +113,9 @@ function next_after_cancel(?Server $server = null) 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, ]); - dispatch(new ApplicationDeploymentJob( + ApplicationDeploymentJob::dispatch( application_deployment_queue_id: $next->id, - ))->onQueue('high'); + ); } break; } diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index 303fcab8e..b568e090c 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -46,7 +46,7 @@ const SPECIFIC_SERVICES = [ // Based on /etc/os-release const SUPPORTED_OS = [ - 'ubuntu debian raspbian', + 'ubuntu debian raspbian pop', 'centos fedora rhel ol rocky amzn almalinux', 'sles opensuse-leap opensuse-tumbleweed', 'arch', diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 52435703c..eda2133a7 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -109,7 +109,8 @@ function format_docker_envs_to_json($rawOutput) function checkMinimumDockerEngineVersion($dockerVersion) { $majorDockerVersion = str($dockerVersion)->before('.')->value(); - if ($majorDockerVersion <= 22) { + $requiredDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.')->value(); + if ($majorDockerVersion < $requiredDockerVersion) { $dockerVersion = null; } @@ -191,7 +192,7 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica { $labels = collect([]); $labels->push('coolify.managed=true'); - $labels->push('coolify.version='.config('version')); + $labels->push('coolify.version='.config('constants.coolify.version')); $labels->push('coolify.'.$type.'Id='.$id); $labels->push("coolify.type=$type"); $labels->push('coolify.name='.$name); @@ -225,16 +226,18 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) case $type?->contains('minio'): $MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first(); $MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first(); + if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) { - return $payload; + return collect([]); } - if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) { - $MINIO_BROWSER_REDIRECT_URL?->update([ + + if (str($MINIO_BROWSER_REDIRECT_URL->value ?? '')->isEmpty()) { + $MINIO_BROWSER_REDIRECT_URL->update([ 'value' => generateFqdn($server, 'console-'.$uuid, true), ]); } - if (is_null($MINIO_SERVER_URL?->value)) { - $MINIO_SERVER_URL?->update([ + if (str($MINIO_SERVER_URL->value ?? '')->isEmpty()) { + $MINIO_SERVER_URL->update([ 'value' => generateFqdn($server, 'minio-'.$uuid, true), ]); } @@ -246,16 +249,18 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) case $type?->contains('logto'): $LOGTO_ENDPOINT = $variables->where('key', 'LOGTO_ENDPOINT')->first(); $LOGTO_ADMIN_ENDPOINT = $variables->where('key', 'LOGTO_ADMIN_ENDPOINT')->first(); + if (is_null($LOGTO_ENDPOINT) || is_null($LOGTO_ADMIN_ENDPOINT)) { - return $payload; + return collect([]); } - if (is_null($LOGTO_ENDPOINT?->value)) { - $LOGTO_ENDPOINT?->update([ + + if (str($LOGTO_ENDPOINT->value ?? '')->isEmpty()) { + $LOGTO_ENDPOINT->update([ 'value' => generateFqdn($server, 'logto-'.$uuid), ]); } - if (is_null($LOGTO_ADMIN_ENDPOINT?->value)) { - $LOGTO_ADMIN_ENDPOINT?->update([ + if (str($LOGTO_ADMIN_ENDPOINT->value ?? '')->isEmpty()) { + $LOGTO_ADMIN_ENDPOINT->update([ 'value' => generateFqdn($server, 'logto-admin-'.$uuid), ]); } @@ -283,6 +288,10 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, $host_without_www = str($host)->replace('www.', ''); $schema = $url->getScheme(); $port = $url->getPort(); + $handle = 'handle_path'; + if (! $is_stripprefix_enabled) { + $handle = 'handle'; + } if (is_null($port) && ! is_null($onlyPort)) { $port = $onlyPort; } @@ -294,11 +303,11 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, $labels->push("caddy_{$loop}.try_files={path} /index.html /index.php"); if ($port) { - $labels->push("caddy_{$loop}.handle_path.{$loop}_reverse_proxy={{upstreams $port}}"); + $labels->push("caddy_{$loop}.{$handle}.{$loop}_reverse_proxy={{upstreams $port}}"); } else { - $labels->push("caddy_{$loop}.handle_path.{$loop}_reverse_proxy={{upstreams}}"); + $labels->push("caddy_{$loop}.{$handle}.{$loop}_reverse_proxy={{upstreams}}"); } - $labels->push("caddy_{$loop}.handle_path={$path}*"); + $labels->push("caddy_{$loop}.{$handle}={$path}*"); if ($is_gzip_enabled) { $labels->push("caddy_{$loop}.encode=zstd gzip"); } @@ -359,8 +368,11 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $https_label = "https-{$loop}-{$uuid}-{$service_name}"; } if (str($image)->contains('ghost')) { - $labels->push("traefik.http.middlewares.redir-ghost.redirectregex.regex=^{$path}/(.*)"); - $labels->push('traefik.http.middlewares.redir-ghost.redirectregex.replacement=/$1'); + $labels->push("traefik.http.middlewares.redir-ghost-{$uuid}.redirectregex.regex=^{$path}/(.*)"); + $labels->push("traefik.http.middlewares.redir-ghost-{$uuid}.redirectregex.replacement=/$1"); + $labels->push("caddy_{$loop}.handle_path.{$loop}_redir-ghost-{$uuid}.handler=rewrite"); + $labels->push("caddy_{$loop}.handle_path.{$loop}_redir-ghost-{$uuid}.rewrite.regexp=^{$path}/(.*)"); + $labels->push("caddy_{$loop}.handle_path.{$loop}_redir-ghost-{$uuid}.rewrite.replacement=/$1"); } $to_www_name = "{$loop}-{$uuid}-to-www"; @@ -394,7 +406,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $middlewares->push('gzip'); } if (str($image)->contains('ghost')) { - $middlewares->push('redir-ghost'); + $middlewares->push("redir-ghost-{$uuid}"); } if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { $labels = $labels->merge($redirect_to_non_www); @@ -417,7 +429,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $middlewares->push('gzip'); } if (str($image)->contains('ghost')) { - $middlewares->push('redir-ghost'); + $middlewares->push("redir-ghost-{$uuid}"); } if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { $labels = $labels->merge($redirect_to_non_www); @@ -466,7 +478,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $middlewares->push('gzip'); } if (str($image)->contains('ghost')) { - $middlewares->push('redir-ghost'); + $middlewares->push("redir-ghost-{$uuid}"); } if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { $labels = $labels->merge($redirect_to_non_www); @@ -489,7 +501,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $middlewares->push('gzip'); } if (str($image)->contains('ghost')) { - $middlewares->push('redir-ghost'); + $middlewares->push("redir-ghost-{$uuid}"); } if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { $labels = $labels->merge($redirect_to_non_www); @@ -654,7 +666,7 @@ function isDatabaseImage(?string $image = null) return false; } -function convert_docker_run_to_compose(?string $custom_docker_run_options = null) +function convertDockerRunToCompose(?string $custom_docker_run_options = null) { $options = []; $compose_options = collect([]); @@ -679,9 +691,17 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null '--privileged' => 'privileged', '--ip' => 'ip', '--shm-size' => 'shm_size', + '--gpus' => 'gpus', ]); foreach ($matches as $match) { $option = $match[1]; + if ($option === '--gpus') { + $regexForParsingDeviceIds = '/device=([0-9A-Za-z-,]+)/'; + preg_match($regexForParsingDeviceIds, $custom_docker_run_options, $device_matches); + $value = $device_matches[1] ?? 'all'; + $options[$option][] = $value; + $options[$option] = array_unique($options[$option]); + } if (isset($match[2]) && $match[2] !== '') { $value = $match[2]; $options[$option][] = $value; @@ -694,7 +714,6 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null $options = collect($options); // Easily get mappings from https://github.com/composerize/composerize/blob/master/packages/composerize/src/mappings.js foreach ($options as $option => $value) { - // ray($option,$value); if (! data_get($mapping, $option)) { continue; } @@ -723,6 +742,28 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null if (! is_null($value) && is_array($value) && count($value) > 0) { $compose_options->put($mapping[$option], $value[0]); } + } elseif ($option === '--gpus') { + $payload = [ + 'driver' => 'nvidia', + 'capabilities' => ['gpu'], + ]; + if (! is_null($value) && is_array($value) && count($value) > 0) { + if (str($value[0]) != 'all') { + if (str($value[0])->contains(',')) { + $payload['device_ids'] = str($value[0])->explode(',')->toArray(); + } else { + $payload['device_ids'] = [$value[0]]; + } + } + } + ray($payload); + $compose_options->put('deploy', [ + 'resources' => [ + 'reservations' => [ + 'devices' => [$payload], + ], + ], + ]); } else { if ($list_options->contains($option)) { if ($compose_options->has($mapping[$option])) { @@ -744,7 +785,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null return $compose_options->toArray(); } -function generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $network) +function generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $network) { $ipv4 = data_get($docker_run_options, 'ip.0'); $ipv6 = data_get($docker_run_options, 'ip6.0'); diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index a8ef0fe5a..463e89b6f 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -173,13 +173,12 @@ function generate_default_proxy_configuration(Server $server) ], 'volumes' => [ '/var/run/docker.sock:/var/run/docker.sock:ro', - "{$proxy_path}:/traefik", + ], 'command' => [ '--ping=true', '--ping.entrypoint=http', '--api.dashboard=true', - '--api.insecure=false', '--entrypoints.http.address=:80', '--entrypoints.https.address=:443', '--entrypoints.http.http.encodequerysemicolons=true', @@ -187,21 +186,26 @@ function generate_default_proxy_configuration(Server $server) '--entrypoints.https.http.encodequerysemicolons=true', '--entryPoints.https.http2.maxConcurrentStreams=50', '--entrypoints.https.http3', - '--providers.docker.exposedbydefault=false', '--providers.file.directory=/traefik/dynamic/', + '--providers.docker.exposedbydefault=false', '--providers.file.watch=true', '--certificatesresolvers.letsencrypt.acme.httpchallenge=true', - '--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json', '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http', + '--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json', ], 'labels' => $labels, ], ], ]; if (isDev()) { - // $config['services']['traefik']['command'][] = "--log.level=debug"; + $config['services']['traefik']['command'][] = '--api.insecure=true'; + $config['services']['traefik']['command'][] = '--log.level=debug'; $config['services']['traefik']['command'][] = '--accesslog.filepath=/traefik/access.log'; $config['services']['traefik']['command'][] = '--accesslog.bufferingsize=100'; + $config['services']['traefik']['volumes'][] = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/:/traefik'; + } else { + $config['services']['traefik']['command'][] = '--api.insecure=false'; + $config['services']['traefik']['volumes'][] = "{$proxy_path}:/traefik"; } if ($server->isSwarm()) { data_forget($config, 'services.traefik.container_name'); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index fb4ae3699..835ead24c 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -7,6 +7,7 @@ use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationPreview; use App\Models\EnvironmentVariable; +use App\Models\GithubApp; use App\Models\InstanceSettings; use App\Models\LocalFileVolume; use App\Models\LocalPersistentVolume; @@ -26,6 +27,7 @@ use App\Models\Team; use App\Models\User; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; +use App\Notifications\Channels\SlackChannel; use App\Notifications\Channels\TelegramChannel; use App\Notifications\Internal\GeneralNotification; use Carbon\CarbonImmutable; @@ -35,6 +37,7 @@ use Illuminate\Mail\Message; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Process\Pool; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Http; @@ -88,8 +91,31 @@ function metrics_dir(): string return base_configuration_dir().'/metrics'; } +function sanitize_string(?string $input = null): ?string +{ + if (is_null($input)) { + return null; + } + // Remove any HTML/PHP tags + $sanitized = strip_tags($input); + + // Convert special characters to HTML entities + $sanitized = htmlspecialchars($sanitized, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + // Remove any control characters + $sanitized = preg_replace('/[\x00-\x1F\x7F]/u', '', $sanitized); + + // Trim whitespace + $sanitized = trim($sanitized); + + return $sanitized; +} + function generate_readme_file(string $name, string $updated_at): string { + $name = sanitize_string($name); + $updated_at = sanitize_string($updated_at); + return "Resource name: $name\nLatest Deployment Date: $updated_at"; } @@ -100,12 +126,12 @@ function isInstanceAdmin() function currentTeam() { - return auth()?->user()?->currentTeam() ?? null; + return Auth::user()?->currentTeam() ?? null; } function showBoarding(): bool { - if (auth()->user()?->isMember()) { + if (Auth::user()?->isMember()) { return false; } @@ -114,14 +140,14 @@ function showBoarding(): bool function refreshSession(?Team $team = null): void { if (! $team) { - if (auth()->user()?->currentTeam()) { - $team = Team::find(auth()->user()->currentTeam()->id); + if (Auth::user()->currentTeam()) { + $team = Team::find(Auth::user()->currentTeam()->id); } else { - $team = User::find(auth()->user()->id)->teams->first(); + $team = User::find(Auth::id())->teams->first(); } } - Cache::forget('team:'.auth()->user()->id); - Cache::remember('team:'.auth()->user()->id, 3600, function () use ($team) { + Cache::forget('team:'.Auth::id()); + Cache::remember('team:'.Auth::id(), 3600, function () use ($team) { return $team; }); session(['currentTeam' => $team]); @@ -357,7 +383,7 @@ function isDev(): bool function isCloud(): bool { - return ! config('coolify.self_hosted'); + return ! config('constants.coolify.self_hosted'); } function translate_cron_expression($expression_to_validate): string @@ -383,6 +409,11 @@ function validate_cron_expression($expression_to_validate): bool return $isValid; } + +function validate_timezone(string $timezone): bool +{ + return in_array($timezone, timezone_identifiers_list()); +} function send_internal_notification(string $message): void { try { @@ -439,11 +470,13 @@ function setNotificationChannels($notifiable, $event) { $channels = []; $isEmailEnabled = isEmailEnabled($notifiable); + $isSlackEnabled = data_get($notifiable, 'slack_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isSubscribedToEmailEvent = data_get($notifiable, "smtp_notifications_$event"); $isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event"); $isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event"); + $isSubscribedToSlackEvent = data_get($notifiable, "slack_notifications_$event"); if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { $channels[] = DiscordChannel::class; @@ -454,6 +487,9 @@ function setNotificationChannels($notifiable, $event) if ($isTelegramEnabled && $isSubscribedToTelegramEvent) { $channels[] = TelegramChannel::class; } + if ($isSlackEnabled && $isSubscribedToSlackEvent) { + $channels[] = SlackChannel::class; + } return $channels; } @@ -933,6 +969,15 @@ function generateEnvValue(string $command, Service|Application|null $service = n case 'REALBASE64_32': $generatedValue = base64_encode(Str::random(32)); break; + case 'HEX_32': + $generatedValue = bin2hex(Str::random(32)); + break; + case 'HEX_64': + $generatedValue = bin2hex(Str::random(64)); + break; + case 'HEX_128': + $generatedValue = bin2hex(Str::random(128)); + break; case 'USER': $generatedValue = Str::random(16); break; @@ -987,7 +1032,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n function getRealtime() { - $envDefined = env('PUSHER_PORT'); + $envDefined = config('constants.pusher.port'); if (empty($envDefined)) { $url = Url::fromString(Request::getSchemeAndHttpHost()); $port = $url->getPort(); @@ -4061,3 +4106,83 @@ function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?calla return $rateLimited; } + +function defaultNginxConfiguration(): string +{ + return 'server { + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri.html $uri/index.html $uri/index.htm $uri/ /index.html /index.htm =404; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + try_files $uri @redirect_to_index; + internal; + } + + error_page 404 = @handle_404; + + location @handle_404 { + root /usr/share/nginx/html; + try_files /404.html @redirect_to_index; + internal; + } + + location @redirect_to_index { + return 302 /; + } +}'; +} + +function convertGitUrl(string $gitRepository, string $deploymentType, ?GithubApp $source = null): array +{ + $repository = $gitRepository; + $providerInfo = [ + 'host' => null, + 'user' => 'git', + 'port' => 22, + 'repository' => $gitRepository, + ]; + $sshMatches = []; + $matches = []; + + // Let's try and parse the string to detect if it's a valid SSH string or not + preg_match('/((.*?)\:\/\/)?(.*@.*:.*)/', $gitRepository, $sshMatches); + + if ($deploymentType === 'deploy_key' && empty($sshMatches) && $source) { + // If this happens, the user may have provided an HTTP URL when they needed an SSH one + // Let's try and fix that for known Git providers + switch ($source->getMorphClass()) { + case \App\Models\GithubApp::class: + $providerInfo['host'] = Url::fromString($source->html_url)->getHost(); + $providerInfo['port'] = $source->custom_port; + $providerInfo['user'] = $source->custom_user; + break; + } + if (! empty($providerInfo['host'])) { + // Until we do not support more providers with App (like GithubApp), this will be always true, port will be 22 + if ($providerInfo['port'] === 22) { + $repository = "{$providerInfo['user']}@{$providerInfo['host']}:{$providerInfo['repository']}"; + } else { + $repository = "ssh://{$providerInfo['user']}@{$providerInfo['host']}:{$providerInfo['port']}/{$providerInfo['repository']}"; + } + } + } + + preg_match('/(?<=:)\d+(?=\/)/', $gitRepository, $matches); + + if (count($matches) === 1) { + $providerInfo['port'] = $matches[0]; + $gitHost = str($gitRepository)->before(':'); + $gitRepo = str($gitRepository)->after('/'); + $repository = "$gitHost:$gitRepo"; + } + + return [ + 'repository' => $repository, + 'port' => $providerInfo['port'], + ]; +} diff --git a/composer.json b/composer.json index 2bae1149c..694bad882 100644 --- a/composer.json +++ b/composer.json @@ -12,14 +12,15 @@ ], "require": { "php": "^8.2", + "3sidedcube/laravel-redoc": "^1.0", "danharrin/livewire-rate-limiting": "^1.1", - "doctrine/dbal": "^3.6", + "doctrine/dbal": "^4.2", "guzzlehttp/guzzle": "^7.5.0", "laravel/fortify": "^1.16.0", - "laravel/framework": "^11", + "laravel/framework": "^11.0", "laravel/horizon": "^5.29.1", "laravel/pail": "^1.1", - "laravel/prompts": "^0.1.6", + "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", "laravel/sanctum": "^4.0", "laravel/socialite": "^5.14.0", "laravel/tinker": "^2.8.1", @@ -27,7 +28,7 @@ "lcobucci/jwt": "^5.0.0", "league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-sftp-v3": "^3.0", - "livewire/livewire": "3.4.9", + "livewire/livewire": "^3.5", "log1x/laravel-webfonts": "^1.0", "lorisleiva/laravel-actions": "^2.7", "nubs/random-name-generator": "^2.2", @@ -36,17 +37,17 @@ "poliander/cron": "^3.0", "purplepixie/phpdns": "^2.1", "pusher/pusher-php-server": "^7.2", - "resend/resend-laravel": "^0.13.0", + "resend/resend-laravel": "^0.15.0", "sentry/sentry-laravel": "^4.6", "socialiteproviders/microsoft-azure": "^5.1", "spatie/laravel-activitylog": "^4.7.3", - "spatie/laravel-data": "^3.4.3", - "spatie/laravel-ray": "^1.32.4", + "spatie/laravel-data": "^4.11", + "spatie/laravel-ray": "^1.37", "spatie/laravel-schemaless-attributes": "^2.4", "spatie/url": "^2.2", - "stripe/stripe-php": "^12.0", - "symfony/yaml": "^6.2", - "visus/cuid2": "^2.0.0", + "stripe/stripe-php": "^16.2.0", + "symfony/yaml": "^7.1.6", + "visus/cuid2": "^4.1.0", "yosymfony/toml": "^1.0", "zircote/swagger-php": "^4.10" }, @@ -58,12 +59,12 @@ "laravel/telescope": "^5.2", "mockery/mockery": "^1.5.1", "nunomaduro/collision": "^8.1", - "pestphp/pest": "^2.16", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.0.19", - "serversideup/spin": "^1.1.0", + "pestphp/pest": "^3.5", + "phpstan/phpstan": "^1.12.10", + "phpunit/phpunit": "^11.4", + "serversideup/spin": "^2.3", "spatie/laravel-ignition": "^2.1.0", - "symfony/http-client": "^6.2" + "symfony/http-client": "^7.1" }, "minimum-stability": "stable", "prefer-stable": true, @@ -119,4 +120,4 @@ "@php artisan key:generate --ansi" ] } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index fb0dcd018..8ea0d9a5a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,66 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3f2342fe6b1ba920c8875f8a8fe41962", + "content-hash": "f50de759f43a3eefb58ce9ebbb02d33b", "packages": [ + { + "name": "3sidedcube/laravel-redoc", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/3sidedcube/laravel-redoc.git", + "reference": "c33a563885dcdf1e0f623df5a56c106d130261da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/3sidedcube/laravel-redoc/zipball/c33a563885dcdf1e0f623df5a56c106d130261da", + "reference": "c33a563885dcdf1e0f623df5a56c106d130261da", + "shasum": "" + }, + "require": { + "illuminate/routing": "^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0", + "php": "^7.4|^8.0|^8.1|^8.2|^8.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.3", + "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "ThreeSidedCube\\LaravelRedoc\\RedocServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "ThreeSidedCube\\LaravelRedoc\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Sherred", + "role": "Developer" + } + ], + "description": "A lightweight package for rendering API documentation using OpenAPI and Redoc.", + "homepage": "https://github.com/3sidedcube/laravel-redoc", + "keywords": [ + "3sidedcube", + "laravel-redoc" + ], + "support": { + "issues": "https://github.com/3sidedcube/laravel-redoc/issues", + "source": "https://github.com/3sidedcube/laravel-redoc/tree/v1.0.1" + }, + "time": "2024-05-20T11:37:55+00:00" + }, { "name": "amphp/amp", "version": "v3.0.2", @@ -867,16 +925,16 @@ }, { "name": "aws/aws-crt-php", - "version": "v1.2.6", + "version": "v1.2.7", "source": { "type": "git", "url": "https://github.com/awslabs/aws-crt-php.git", - "reference": "a63485b65b6b3367039306496d49737cf1995408" + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/a63485b65b6b3367039306496d49737cf1995408", - "reference": "a63485b65b6b3367039306496d49737cf1995408", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e", "shasum": "" }, "require": { @@ -915,22 +973,22 @@ ], "support": { "issues": "https://github.com/awslabs/aws-crt-php/issues", - "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.6" + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7" }, - "time": "2024-06-13T17:21:28+00:00" + "time": "2024-10-18T22:15:13+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.324.0", + "version": "3.327.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "b258712f0d986e00e1143d55246b6f9e344c7184" + "reference": "3d52ec587989b136e486f94eff3dd316465aeb42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b258712f0d986e00e1143d55246b6f9e344c7184", - "reference": "b258712f0d986e00e1143d55246b6f9e344c7184", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3d52ec587989b136e486f94eff3dd316465aeb42", + "reference": "3d52ec587989b136e486f94eff3dd316465aeb42", "shasum": "" }, "require": { @@ -1013,9 +1071,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.324.0" + "source": "https://github.com/aws/aws-sdk-php/tree/3.327.1" }, - "time": "2024-10-10T18:06:36+00:00" + "time": "2024-11-15T01:53:30+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1133,26 +1191,26 @@ }, { "name": "carbonphp/carbon-doctrine-types", - "version": "2.1.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", - "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb" + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", - "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": "^8.1" }, "conflict": { - "doctrine/dbal": "<3.7.0 || >=4.0.0" + "doctrine/dbal": "<4.0.0 || >=5.0.0" }, "require-dev": { - "doctrine/dbal": "^3.7.0", + "doctrine/dbal": "^4.0.0", "nesbot/carbon": "^2.71.0 || ^3.0.0", "phpunit/phpunit": "^10.3" }, @@ -1182,7 +1240,7 @@ ], "support": { "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", - "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.1.0" + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" }, "funding": [ { @@ -1198,7 +1256,7 @@ "type": "tidelift" } ], - "time": "2023-12-11T17:09:12+00:00" + "time": "2024-02-09T16:56:22+00:00" }, { "name": "danharrin/livewire-rate-limiting", @@ -1423,142 +1481,44 @@ }, "time": "2024-07-08T12:26:09+00:00" }, - { - "name": "doctrine/cache", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/cache.git", - "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", - "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", - "shasum": "" - }, - "require": { - "php": "~7.1 || ^8.0" - }, - "conflict": { - "doctrine/common": ">2.2,<2.4" - }, - "require-dev": { - "cache/integration-tests": "dev-master", - "doctrine/coding-standard": "^9", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psr/cache": "^1.0 || ^2.0 || ^3.0", - "symfony/cache": "^4.4 || ^5.4 || ^6", - "symfony/var-exporter": "^4.4 || ^5.4 || ^6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", - "homepage": "https://www.doctrine-project.org/projects/cache.html", - "keywords": [ - "abstraction", - "apcu", - "cache", - "caching", - "couchdb", - "memcached", - "php", - "redis", - "xcache" - ], - "support": { - "issues": "https://github.com/doctrine/cache/issues", - "source": "https://github.com/doctrine/cache/tree/2.2.0" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", - "type": "tidelift" - } - ], - "time": "2022-05-20T20:07:39+00:00" - }, { "name": "doctrine/dbal", - "version": "3.9.3", + "version": "4.2.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba" + "reference": "dadd35300837a3a2184bd47d403333b15d0a9bd0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", - "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/dadd35300837a3a2184bd47d403333b15d0a9bd0", + "reference": "dadd35300837a3a2184bd47d403333b15d0a9bd0", "shasum": "" }, "require": { - "composer-runtime-api": "^2", - "doctrine/cache": "^1.11|^2.0", "doctrine/deprecations": "^0.5.3|^1", - "doctrine/event-manager": "^1|^2", - "php": "^7.4 || ^8.0", + "php": "^8.1", "psr/cache": "^1|^2|^3", "psr/log": "^1|^2|^3" }, "require-dev": { "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", - "jetbrains/phpstorm-stubs": "2023.1", + "jetbrains/phpstorm-stubs": "2023.2", "phpstan/phpstan": "1.12.6", + "phpstan/phpstan-phpunit": "1.4.0", "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "9.6.20", - "psalm/plugin-phpunit": "0.18.4", + "phpunit/phpunit": "10.5.30", + "psalm/plugin-phpunit": "0.19.0", "slevomat/coding-standard": "8.13.1", "squizlabs/php_codesniffer": "3.10.2", - "symfony/cache": "^5.4|^6.0|^7.0", - "symfony/console": "^4.4|^5.4|^6.0|^7.0", - "vimeo/psalm": "4.30.0" + "symfony/cache": "^6.3.8|^7.0", + "symfony/console": "^5.4|^6.3|^7.0", + "vimeo/psalm": "5.25.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." }, - "bin": [ - "bin/doctrine-dbal" - ], "type": "library", "autoload": { "psr-4": { @@ -1611,7 +1571,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.9.3" + "source": "https://github.com/doctrine/dbal/tree/4.2.1" }, "funding": [ { @@ -1627,7 +1587,7 @@ "type": "tidelift" } ], - "time": "2024-10-10T17:56:43+00:00" + "time": "2024-10-10T18:01:27+00:00" }, { "name": "doctrine/deprecations", @@ -1676,97 +1636,6 @@ }, "time": "2024-01-30T19:34:25+00:00" }, - { - "name": "doctrine/event-manager", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/event-manager.git", - "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", - "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", - "shasum": "" - }, - "require": { - "php": "^8.1" - }, - "conflict": { - "doctrine/common": "<2.9" - }, - "require-dev": { - "doctrine/coding-standard": "^12", - "phpstan/phpstan": "^1.8.8", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.24" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - }, - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" - } - ], - "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", - "homepage": "https://www.doctrine-project.org/projects/event-manager.html", - "keywords": [ - "event", - "event dispatcher", - "event manager", - "event system", - "events" - ], - "support": { - "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/2.0.1" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", - "type": "tidelift" - } - ], - "time": "2024-05-22T20:47:39+00:00" - }, { "name": "doctrine/inflector", "version": "2.0.10", @@ -2391,16 +2260,16 @@ }, { "name": "guzzlehttp/promises", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", - "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", "shasum": "" }, "require": { @@ -2454,7 +2323,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.3" + "source": "https://github.com/guzzle/promises/tree/2.0.4" }, "funding": [ { @@ -2470,7 +2339,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T10:29:17+00:00" + "time": "2024-10-17T10:06:22+00:00" }, { "name": "guzzlehttp/psr7", @@ -2793,16 +2662,16 @@ }, { "name": "laravel/fortify", - "version": "v1.24.2", + "version": "v1.24.5", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "42695c45087e5abb3e173725b4f1ef4956a7b47d" + "reference": "bba8c2ecc3fcc78e8632e0d719ae10bef6343eef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/42695c45087e5abb3e173725b4f1ef4956a7b47d", - "reference": "42695c45087e5abb3e173725b4f1ef4956a7b47d", + "url": "https://api.github.com/repos/laravel/fortify/zipball/bba8c2ecc3fcc78e8632e0d719ae10bef6343eef", + "reference": "bba8c2ecc3fcc78e8632e0d719ae10bef6343eef", "shasum": "" }, "require": { @@ -2854,20 +2723,20 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2024-09-16T19:20:52+00:00" + "time": "2024-11-12T14:51:12+00:00" }, { "name": "laravel/framework", - "version": "v11.27.2", + "version": "v11.31.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9" + "reference": "365090ed2c68244e3141cdb5e247cdf3dfba2c40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9", - "reference": "a51d1f2b771c542324a3d9b76a98b1bbc75c0ee9", + "url": "https://api.github.com/repos/laravel/framework/zipball/365090ed2c68244e3141cdb5e247cdf3dfba2c40", + "reference": "365090ed2c68244e3141cdb5e247cdf3dfba2c40", "shasum": "" }, "require": { @@ -3063,20 +2932,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-10-09T04:17:35+00:00" + "time": "2024-11-12T15:36:15+00:00" }, { "name": "laravel/horizon", - "version": "v5.29.1", + "version": "v5.29.3", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "9f482f21c23ed01c2366d1157843165165579c23" + "reference": "a48d242759704e598242074daf0060bbeb6ed46d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/9f482f21c23ed01c2366d1157843165165579c23", - "reference": "9f482f21c23ed01c2366d1157843165165579c23", + "url": "https://api.github.com/repos/laravel/horizon/zipball/a48d242759704e598242074daf0060bbeb6ed46d", + "reference": "a48d242759704e598242074daf0060bbeb6ed46d", "shasum": "" }, "require": { @@ -3091,6 +2960,7 @@ "ramsey/uuid": "^4.0", "symfony/console": "^6.0|^7.0", "symfony/error-handler": "^6.0|^7.0", + "symfony/polyfill-php83": "^1.28", "symfony/process": "^6.0|^7.0" }, "require-dev": { @@ -3140,22 +3010,22 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.29.1" + "source": "https://github.com/laravel/horizon/tree/v5.29.3" }, - "time": "2024-10-08T18:23:02+00:00" + "time": "2024-11-07T21:51:45+00:00" }, { "name": "laravel/pail", - "version": "v1.1.5", + "version": "v1.2.1", "source": { "type": "git", "url": "https://github.com/laravel/pail.git", - "reference": "b33ad8321416fe86efed7bf398f3306c47b4871b" + "reference": "353ac12134b98e2e7c3333d916bd3e523931e583" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/b33ad8321416fe86efed7bf398f3306c47b4871b", - "reference": "b33ad8321416fe86efed7bf398f3306c47b4871b", + "url": "https://api.github.com/repos/laravel/pail/zipball/353ac12134b98e2e7c3333d916bd3e523931e583", + "reference": "353ac12134b98e2e7c3333d916bd3e523931e583", "shasum": "" }, "require": { @@ -3170,8 +3040,9 @@ "symfony/console": "^6.0|^7.0" }, "require-dev": { + "laravel/framework": "^10.24|^11.0", "laravel/pint": "^1.13", - "orchestra/testbench": "^8.12|^9.0", + "orchestra/testbench-core": "^8.12|^9.0", "pestphp/pest": "^2.20", "pestphp/pest-plugin-type-coverage": "^2.3", "phpstan/phpstan": "^1.10", @@ -3219,25 +3090,25 @@ "issues": "https://github.com/laravel/pail/issues", "source": "https://github.com/laravel/pail" }, - "time": "2024-10-15T20:06:24+00:00" + "time": "2024-10-23T12:56:23+00:00" }, { "name": "laravel/prompts", - "version": "v0.1.25", + "version": "v0.3.2", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95" + "reference": "0e0535747c6b8d6d10adca8b68293cf4517abb0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/7b4029a84c37cb2725fc7f011586e2997040bc95", - "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95", + "url": "https://api.github.com/repos/laravel/prompts/zipball/0e0535747c6b8d6d10adca8b68293cf4517abb0f", + "reference": "0e0535747c6b8d6d10adca8b68293cf4517abb0f", "shasum": "" }, "require": { + "composer-runtime-api": "^2.2", "ext-mbstring": "*", - "illuminate/collections": "^10.0|^11.0", "php": "^8.1", "symfony/console": "^6.2|^7.0" }, @@ -3246,8 +3117,9 @@ "laravel/framework": ">=10.17.0 <10.25.0" }, "require-dev": { + "illuminate/collections": "^10.0|^11.0", "mockery/mockery": "^1.5", - "pestphp/pest": "^2.3", + "pestphp/pest": "^2.3|^3.4", "phpstan/phpstan": "^1.11", "phpstan/phpstan-mockery": "^1.1" }, @@ -3257,7 +3129,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "0.1.x-dev" + "dev-main": "0.3.x-dev" } }, "autoload": { @@ -3275,9 +3147,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.1.25" + "source": "https://github.com/laravel/prompts/tree/v0.3.2" }, - "time": "2024-08-12T22:06:33+00:00" + "time": "2024-11-12T14:59:47+00:00" }, { "name": "laravel/sanctum", @@ -3345,16 +3217,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v1.3.5", + "version": "v1.3.6", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c" + "reference": "f865a58ea3a0107c336b7045104c75243fa59d96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c", - "reference": "1dc4a3dbfa2b7628a3114e43e32120cce7cdda9c", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/f865a58ea3a0107c336b7045104c75243fa59d96", + "reference": "f865a58ea3a0107c336b7045104c75243fa59d96", "shasum": "" }, "require": { @@ -3402,7 +3274,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2024-09-23T13:33:08+00:00" + "time": "2024-11-11T17:06:04+00:00" }, { "name": "laravel/socialite", @@ -3607,16 +3479,16 @@ }, { "name": "lcobucci/jwt", - "version": "5.4.0", + "version": "5.4.2", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "aac4fd512681fd5cb4b77d2105ab7ec700c72051" + "reference": "ea1ce71cbf9741e445a5914e2f67cdbb484ff712" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/aac4fd512681fd5cb4b77d2105ab7ec700c72051", - "reference": "aac4fd512681fd5cb4b77d2105ab7ec700c72051", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/ea1ce71cbf9741e445a5914e2f67cdbb484ff712", + "reference": "ea1ce71cbf9741e445a5914e2f67cdbb484ff712", "shasum": "" }, "require": { @@ -3664,7 +3536,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/5.4.0" + "source": "https://github.com/lcobucci/jwt/tree/5.4.2" }, "funding": [ { @@ -3676,7 +3548,7 @@ "type": "patreon" } ], - "time": "2024-10-08T22:06:45+00:00" + "time": "2024-11-07T12:54:35+00:00" }, { "name": "league/commonmark", @@ -4410,16 +4282,16 @@ }, { "name": "livewire/livewire", - "version": "v3.4.9", + "version": "v3.5.12", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "c65b3f0798ab2c9338213ede3588c3cdf4e6fcc0" + "reference": "3c8d1f9d7d9098aaea663093ae168f2d5d2ae73d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/c65b3f0798ab2c9338213ede3588c3cdf4e6fcc0", - "reference": "c65b3f0798ab2c9338213ede3588c3cdf4e6fcc0", + "url": "https://api.github.com/repos/livewire/livewire/zipball/3c8d1f9d7d9098aaea663093ae168f2d5d2ae73d", + "reference": "3c8d1f9d7d9098aaea663093ae168f2d5d2ae73d", "shasum": "" }, "require": { @@ -4427,17 +4299,18 @@ "illuminate/routing": "^10.0|^11.0", "illuminate/support": "^10.0|^11.0", "illuminate/validation": "^10.0|^11.0", + "laravel/prompts": "^0.1.24|^0.2|^0.3", "league/mime-type-detection": "^1.9", "php": "^8.1", + "symfony/console": "^6.0|^7.0", "symfony/http-kernel": "^6.2|^7.0" }, "require-dev": { "calebporzio/sushi": "^2.1", - "laravel/framework": "^10.0|^11.0", - "laravel/prompts": "^0.1.6", + "laravel/framework": "^10.15.0|^11.0", "mockery/mockery": "^1.3.1", - "orchestra/testbench": "8.20.0|^9.0", - "orchestra/testbench-dusk": "8.20.0|^9.0", + "orchestra/testbench": "^8.21.0|^9.0", + "orchestra/testbench-dusk": "^8.24|^9.1", "phpunit/phpunit": "^10.4", "psy/psysh": "^0.11.22|^0.12" }, @@ -4473,7 +4346,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.4.9" + "source": "https://github.com/livewire/livewire/tree/v3.5.12" }, "funding": [ { @@ -4481,31 +4354,31 @@ "type": "github" } ], - "time": "2024-03-14T14:03:32+00:00" + "time": "2024-10-15T19:35:06+00:00" }, { "name": "log1x/laravel-webfonts", - "version": "v1.0.1", + "version": "v1.0.2", "source": { "type": "git", "url": "https://github.com/Log1x/laravel-webfonts.git", - "reference": "0d38122aa7f5501394006a6715f7d97dac223507" + "reference": "128a20af26f02db84df21abc6524e5a069cf20a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Log1x/laravel-webfonts/zipball/0d38122aa7f5501394006a6715f7d97dac223507", - "reference": "0d38122aa7f5501394006a6715f7d97dac223507", + "url": "https://api.github.com/repos/Log1x/laravel-webfonts/zipball/128a20af26f02db84df21abc6524e5a069cf20a4", + "reference": "128a20af26f02db84df21abc6524e5a069cf20a4", "shasum": "" }, "require": { "guzzlehttp/guzzle": "^7.8", - "laravel/prompts": "^0.1.15", + "laravel/prompts": "^0.1|^0.2|^0.3", "php": ">=8.1" }, "require-dev": { - "illuminate/console": "^10.41", - "illuminate/http": "^10.41", - "illuminate/support": "^10.41", + "illuminate/console": "^10.0|^11.0", + "illuminate/http": "^10.0|^11.0", + "illuminate/support": "^10.0|^11.0", "laravel/pint": "^1.13" }, "type": "package", @@ -4535,7 +4408,7 @@ "description": "Download, install, and preload over 1500 Google fonts locally in your Laravel project", "support": { "issues": "https://github.com/Log1x/laravel-webfonts/issues", - "source": "https://github.com/Log1x/laravel-webfonts/tree/v1.0.1" + "source": "https://github.com/Log1x/laravel-webfonts/tree/v1.0.2" }, "funding": [ { @@ -4543,7 +4416,7 @@ "type": "github" } ], - "time": "2024-03-28T11:53:11+00:00" + "time": "2024-11-12T19:00:31+00:00" }, { "name": "lorisleiva/laravel-actions", @@ -4695,16 +4568,16 @@ }, { "name": "monolog/monolog", - "version": "3.7.0", + "version": "3.8.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8" + "reference": "32e515fdc02cdafbe4593e30a9350d486b125b67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f4393b648b78a5408747de94fca38beb5f7e9ef8", - "reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/32e515fdc02cdafbe4593e30a9350d486b125b67", + "reference": "32e515fdc02cdafbe4593e30a9350d486b125b67", "shasum": "" }, "require": { @@ -4724,12 +4597,14 @@ "guzzlehttp/psr7": "^2.2", "mongodb/mongodb": "^1.8", "php-amqplib/php-amqplib": "~2.4 || ^3", - "phpstan/phpstan": "^1.9", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-strict-rules": "^1.4", - "phpunit/phpunit": "^10.5.17", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", "predis/predis": "^1.1 || ^2", - "ruflin/elastica": "^7", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", "symfony/mailer": "^5.4 || ^6", "symfony/mime": "^5.4 || ^6" }, @@ -4780,7 +4655,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.7.0" + "source": "https://github.com/Seldaek/monolog/tree/3.8.0" }, "funding": [ { @@ -4792,7 +4667,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:40:51+00:00" + "time": "2024-11-12T13:57:08+00:00" }, { "name": "mtdowling/jmespath.php", @@ -4862,20 +4737,20 @@ }, { "name": "nesbot/carbon", - "version": "3.8.0", + "version": "3.8.2", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "bbd3eef89af8ba66a3aa7952b5439168fbcc529f" + "reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/bbd3eef89af8ba66a3aa7952b5439168fbcc529f", - "reference": "bbd3eef89af8ba66a3aa7952b5439168fbcc529f", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e1268cdbc486d97ce23fef2c666dc3c6b6de9947", + "reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947", "shasum": "" }, "require": { - "carbonphp/carbon-doctrine-types": "*", + "carbonphp/carbon-doctrine-types": "<100.0", "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", @@ -4964,7 +4839,7 @@ "type": "tidelift" } ], - "time": "2024-08-19T06:22:39+00:00" + "time": "2024-11-07T17:46:48+00:00" }, { "name": "nette/schema", @@ -5227,32 +5102,31 @@ }, { "name": "nunomaduro/termwind", - "version": "v2.1.0", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "e5f21eade88689536c0cdad4c3cd75f3ed26e01a" + "reference": "42c84e4e8090766bbd6445d06cd6e57650626ea3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/e5f21eade88689536c0cdad4c3cd75f3ed26e01a", - "reference": "e5f21eade88689536c0cdad4c3cd75f3ed26e01a", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/42c84e4e8090766bbd6445d06cd6e57650626ea3", + "reference": "42c84e4e8090766bbd6445d06cd6e57650626ea3", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^8.2", - "symfony/console": "^7.0.4" + "symfony/console": "^7.1.5" }, "require-dev": { - "ergebnis/phpstan-rules": "^2.2.0", - "illuminate/console": "^11.1.1", - "laravel/pint": "^1.15.0", - "mockery/mockery": "^1.6.11", - "pestphp/pest": "^2.34.6", - "phpstan/phpstan": "^1.10.66", - "phpstan/phpstan-strict-rules": "^1.5.2", - "symfony/var-dumper": "^7.0.4", + "illuminate/console": "^11.28.0", + "laravel/pint": "^1.18.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0", + "phpstan/phpstan": "^1.12.6", + "phpstan/phpstan-strict-rules": "^1.6.1", + "symfony/var-dumper": "^7.1.5", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -5295,7 +5169,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.1.0" + "source": "https://github.com/nunomaduro/termwind/tree/v2.2.0" }, "funding": [ { @@ -5311,7 +5185,7 @@ "type": "github" } ], - "time": "2024-09-05T15:25:50+00:00" + "time": "2024-10-15T16:15:16+00:00" }, { "name": "nyholm/psr7", @@ -5510,30 +5384,35 @@ }, { "name": "paragonie/sodium_compat", - "version": "v1.21.1", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "bb312875dcdd20680419564fe42ba1d9564b9e37" + "reference": "a673d5f310477027cead2e2f2b6db5d8368157cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/bb312875dcdd20680419564fe42ba1d9564b9e37", - "reference": "bb312875dcdd20680419564fe42ba1d9564b9e37", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/a673d5f310477027cead2e2f2b6db5d8368157cb", + "reference": "a673d5f310477027cead2e2f2b6db5d8368157cb", "shasum": "" }, "require": { - "paragonie/random_compat": ">=1", - "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" + "php": "^8.1", + "php-64bit": "*" }, "require-dev": { - "phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9" + "phpunit/phpunit": "^7|^8|^9", + "vimeo/psalm": "^4|^5" }, "suggest": { - "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.", - "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + "ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "files": [ "autoload.php" @@ -5590,9 +5469,9 @@ ], "support": { "issues": "https://github.com/paragonie/sodium_compat/issues", - "source": "https://github.com/paragonie/sodium_compat/tree/v1.21.1" + "source": "https://github.com/paragonie/sodium_compat/tree/v2.1.0" }, - "time": "2024-04-22T22:05:04+00:00" + "time": "2024-09-04T12:51:01+00:00" }, { "name": "php-di/invoker", @@ -5722,6 +5601,73 @@ ], "time": "2024-07-21T15:55:45+00:00" }, + { + "name": "phpdocumentor/reflection", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/Reflection.git", + "reference": "61e2f1fe7683e9647b9ed8d9e53d08699385267d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/Reflection/zipball/61e2f1fe7683e9647b9ed8d9e53d08699385267d", + "reference": "61e2f1fe7683e9647b9ed8d9e53d08699385267d", + "shasum": "" + }, + "require": { + "nikic/php-parser": "~4.18 || ^5.0", + "php": "8.1.*|8.2.*|8.3.*", + "phpdocumentor/reflection-common": "^2.1", + "phpdocumentor/reflection-docblock": "^5", + "phpdocumentor/type-resolver": "^1.2", + "symfony/polyfill-php80": "^1.28", + "webmozart/assert": "^1.7" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "doctrine/coding-standard": "^12.0", + "mikey179/vfsstream": "~1.2", + "mockery/mockery": "~1.6.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^10.0", + "psalm/phar": "^5.24", + "rector/rector": "^1.0.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-5.x": "5.3.x-dev", + "dev-6.x": "6.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\": "src/phpDocumentor" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Reflection library to do Static Analysis for PHP Projects", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/Reflection/issues", + "source": "https://github.com/phpDocumentor/Reflection/tree/6.0.0" + }, + "time": "2024-05-23T19:28:12+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -5776,24 +5722,88 @@ "time": "2020-06-27T09:03:43+00:00" }, { - "name": "phpdocumentor/type-resolver", - "version": "1.8.2", + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.0", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "153ae662783729388a584b4361f2545e4d841e3c" + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "f3558a4c23426d12bffeaab463f8a8d8b681193c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c", - "reference": "153ae662783729388a584b4361f2545e4d841e3c", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/f3558a4c23426d12bffeaab463f8a8d8b681193c", + "reference": "f3558a4c23426d12bffeaab463f8a8d8b681193c", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.0" + }, + "time": "2024-11-12T11:25:25+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", "shasum": "" }, "require": { "doctrine/deprecations": "^1.0", "php": "^7.3 || ^8.0", "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.13" + "phpstan/phpdoc-parser": "^1.18|^2.0" }, "require-dev": { "ext-tokenizer": "*", @@ -5829,9 +5839,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" }, - "time": "2024-02-23T11:10:43+00:00" + "time": "2024-11-09T15:12:26+00:00" }, { "name": "phpoption/phpoption", @@ -6020,30 +6030,30 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.32.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4" + "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6ca22b154efdd9e3c68c56f5d94670920a1c19a4", - "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/c00d78fb6b29658347f9d37ebe104bffadf36299", + "reference": "c00d78fb6b29658347f9d37ebe104bffadf36299", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { "doctrine/annotations": "^2.0", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^5.3.0", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.5", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", "symfony/process": "^5.2" }, "type": "library", @@ -6061,22 +6071,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.32.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.0" }, - "time": "2024-09-26T07:23:32+00:00" + "time": "2024-10-13T11:29:49+00:00" }, { "name": "phpstan/phpstan", - "version": "1.12.6", + "version": "1.12.10", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae" + "reference": "fc463b5d0fe906dcf19689be692c65c50406a071" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc4d2f145a88ea7141ae698effd64d9df46527ae", - "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fc463b5d0fe906dcf19689be692c65c50406a071", + "reference": "fc463b5d0fe906dcf19689be692c65c50406a071", "shasum": "" }, "require": { @@ -6121,7 +6131,7 @@ "type": "github" } ], - "time": "2024-10-06T15:03:59+00:00" + "time": "2024-11-11T15:37:09+00:00" }, { "name": "pion/laravel-chunk-upload", @@ -6875,23 +6885,23 @@ }, { "name": "pusher/pusher-php-server", - "version": "7.2.4", + "version": "7.2.6", "source": { "type": "git", "url": "https://github.com/pusher/pusher-http-php.git", - "reference": "de2f72296808f9cafa6a4462b15a768ff130cddb" + "reference": "d89e9997191d18fb0fe03a956fa3ccfe0af524ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/de2f72296808f9cafa6a4462b15a768ff130cddb", - "reference": "de2f72296808f9cafa6a4462b15a768ff130cddb", + "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/d89e9997191d18fb0fe03a956fa3ccfe0af524ea", + "reference": "d89e9997191d18fb0fe03a956fa3ccfe0af524ea", "shasum": "" }, "require": { "ext-curl": "*", "ext-json": "*", "guzzlehttp/guzzle": "^7.2", - "paragonie/sodium_compat": "^1.6", + "paragonie/sodium_compat": "^1.6|^2.0", "php": "^7.3|^8.0", "psr/log": "^1.0|^2.0|^3.0" }, @@ -6930,9 +6940,9 @@ ], "support": { "issues": "https://github.com/pusher/pusher-http-php/issues", - "source": "https://github.com/pusher/pusher-http-php/tree/7.2.4" + "source": "https://github.com/pusher/pusher-http-php/tree/7.2.6" }, - "time": "2023-12-15T10:58:53+00:00" + "time": "2024-10-18T12:04:31+00:00" }, { "name": "ralouphie/getallheaders", @@ -7161,16 +7171,16 @@ }, { "name": "rector/rector", - "version": "1.2.6", + "version": "1.2.10", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99" + "reference": "40f9cf38c05296bd32f444121336a521a293fa61" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/6ca85da28159dbd3bb36211c5104b7bc91278e99", - "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/40f9cf38c05296bd32f444121336a521a293fa61", + "reference": "40f9cf38c05296bd32f444121336a521a293fa61", "shasum": "" }, "require": { @@ -7208,7 +7218,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/1.2.6" + "source": "https://github.com/rectorphp/rector/tree/1.2.10" }, "funding": [ { @@ -7216,27 +7226,27 @@ "type": "github" } ], - "time": "2024-10-03T08:56:44+00:00" + "time": "2024-11-08T13:59:10+00:00" }, { "name": "resend/resend-laravel", - "version": "v0.13.0", + "version": "v0.15.0", "source": { "type": "git", "url": "https://github.com/resend/resend-laravel.git", - "reference": "23aed22df0d0b23c2952da2aaed6a8b88d301a8a" + "reference": "af914817abc6abaa4522b5cfb177f3519493fd6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/resend/resend-laravel/zipball/23aed22df0d0b23c2952da2aaed6a8b88d301a8a", - "reference": "23aed22df0d0b23c2952da2aaed6a8b88d301a8a", + "url": "https://api.github.com/repos/resend/resend-laravel/zipball/af914817abc6abaa4522b5cfb177f3519493fd6e", + "reference": "af914817abc6abaa4522b5cfb177f3519493fd6e", "shasum": "" }, "require": { "illuminate/http": "^10.0|^11.0", "illuminate/support": "^10.0|^11.0", "php": "^8.1", - "resend/resend-php": "^0.12.0", + "resend/resend-php": "^0.14.0", "symfony/mailer": "^6.2|^7.0" }, "require-dev": { @@ -7283,22 +7293,22 @@ ], "support": { "issues": "https://github.com/resend/resend-laravel/issues", - "source": "https://github.com/resend/resend-laravel/tree/v0.13.0" + "source": "https://github.com/resend/resend-laravel/tree/v0.15.0" }, - "time": "2024-07-08T18:51:42+00:00" + "time": "2024-11-04T18:34:08+00:00" }, { "name": "resend/resend-php", - "version": "v0.12.0", + "version": "v0.14.0", "source": { "type": "git", "url": "https://github.com/resend/resend-php.git", - "reference": "37fb79bb8160ce2de521bf37484ba59e89236521" + "reference": "d7900752bb9839421d40d9e66362bffb3ec07aac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/resend/resend-php/zipball/37fb79bb8160ce2de521bf37484ba59e89236521", - "reference": "37fb79bb8160ce2de521bf37484ba59e89236521", + "url": "https://api.github.com/repos/resend/resend-php/zipball/d7900752bb9839421d40d9e66362bffb3ec07aac", + "reference": "d7900752bb9839421d40d9e66362bffb3ec07aac", "shasum": "" }, "require": { @@ -7340,9 +7350,9 @@ ], "support": { "issues": "https://github.com/resend/resend-php/issues", - "source": "https://github.com/resend/resend-php/tree/v0.12.0" + "source": "https://github.com/resend/resend-php/tree/v0.14.0" }, - "time": "2024-03-04T03:16:28+00:00" + "time": "2024-11-01T02:00:44+00:00" }, { "name": "revolt/event-loop", @@ -7418,16 +7428,16 @@ }, { "name": "sentry/sentry", - "version": "4.9.0", + "version": "4.10.0", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-php.git", - "reference": "788ec170f51ebb22f2809a1e3f78b19ccd39b70d" + "reference": "2af937d47d8aadb8dab0b1d7b9557e495dd12856" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/788ec170f51ebb22f2809a1e3f78b19ccd39b70d", - "reference": "788ec170f51ebb22f2809a1e3f78b19ccd39b70d", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/2af937d47d8aadb8dab0b1d7b9557e495dd12856", + "reference": "2af937d47d8aadb8dab0b1d7b9557e495dd12856", "shasum": "" }, "require": { @@ -7445,12 +7455,12 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.4", - "guzzlehttp/promises": "^1.0|^2.0", + "guzzlehttp/promises": "^2.0.3", "guzzlehttp/psr7": "^1.8.4|^2.1.1", "monolog/monolog": "^1.6|^2.0|^3.0", "phpbench/phpbench": "^1.0", "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^8.5.14|^9.4", + "phpunit/phpunit": "^8.5|^9.6", "symfony/phpunit-bridge": "^5.2|^6.0|^7.0", "vimeo/psalm": "^4.17" }, @@ -7491,7 +7501,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-php/issues", - "source": "https://github.com/getsentry/sentry-php/tree/4.9.0" + "source": "https://github.com/getsentry/sentry-php/tree/4.10.0" }, "funding": [ { @@ -7503,27 +7513,27 @@ "type": "custom" } ], - "time": "2024-08-08T14:40:50+00:00" + "time": "2024-11-06T07:44:19+00:00" }, { "name": "sentry/sentry-laravel", - "version": "4.9.0", + "version": "4.10.0", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-laravel.git", - "reference": "73078e1f26d57f7a10e3bee2a2f543a02f6493c3" + "reference": "cbdd224cc5a224528bf6b19507ad76187b3bccfa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/73078e1f26d57f7a10e3bee2a2f543a02f6493c3", - "reference": "73078e1f26d57f7a10e3bee2a2f543a02f6493c3", + "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/cbdd224cc5a224528bf6b19507ad76187b3bccfa", + "reference": "cbdd224cc5a224528bf6b19507ad76187b3bccfa", "shasum": "" }, "require": { "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0", "nyholm/psr7": "^1.0", "php": "^7.2 | ^8.0", - "sentry/sentry": "^4.9", + "sentry/sentry": "^4.10", "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0" }, "require-dev": { @@ -7580,7 +7590,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-laravel/issues", - "source": "https://github.com/getsentry/sentry-laravel/tree/4.9.0" + "source": "https://github.com/getsentry/sentry-laravel/tree/4.10.0" }, "funding": [ { @@ -7592,20 +7602,20 @@ "type": "custom" } ], - "time": "2024-09-19T12:58:53+00:00" + "time": "2024-11-07T08:05:24+00:00" }, { "name": "socialiteproviders/manager", - "version": "v4.6.0", + "version": "v4.7.0", "source": { "type": "git", "url": "https://github.com/SocialiteProviders/Manager.git", - "reference": "dea5190981c31b89e52259da9ab1ca4e2b258b21" + "reference": "ab0691b82cec77efd90154c78f1854903455c82f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/dea5190981c31b89e52259da9ab1ca4e2b258b21", - "reference": "dea5190981c31b89e52259da9ab1ca4e2b258b21", + "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/ab0691b82cec77efd90154c78f1854903455c82f", + "reference": "ab0691b82cec77efd90154c78f1854903455c82f", "shasum": "" }, "require": { @@ -7666,7 +7676,7 @@ "issues": "https://github.com/socialiteproviders/manager/issues", "source": "https://github.com/socialiteproviders/manager" }, - "time": "2024-05-04T07:57:39+00:00" + "time": "2024-11-10T01:56:18+00:00" }, { "name": "socialiteproviders/microsoft-azure", @@ -7784,16 +7794,16 @@ }, { "name": "spatie/laravel-activitylog", - "version": "4.8.0", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-activitylog.git", - "reference": "eb6f37dd40af950ce10cf5280f0acfa3e08c3bff" + "reference": "e0fc28178515a5396f48e107ed697719189bbe02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/eb6f37dd40af950ce10cf5280f0acfa3e08c3bff", - "reference": "eb6f37dd40af950ce10cf5280f0acfa3e08c3bff", + "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/e0fc28178515a5396f48e107ed697719189bbe02", + "reference": "e0fc28178515a5396f48e107ed697719189bbe02", "shasum": "" }, "require": { @@ -7859,7 +7869,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-activitylog/issues", - "source": "https://github.com/spatie/laravel-activitylog/tree/4.8.0" + "source": "https://github.com/spatie/laravel-activitylog/tree/4.9.0" }, "funding": [ { @@ -7871,47 +7881,47 @@ "type": "github" } ], - "time": "2024-03-08T22:28:17+00:00" + "time": "2024-10-18T13:38:47+00:00" }, { "name": "spatie/laravel-data", - "version": "3.12.0", + "version": "4.11.1", "source": { "type": "git", "url": "https://github.com/spatie/laravel-data.git", - "reference": "d44e04839407bc32b029be59ba80090a5f720e91" + "reference": "df5b58baebae34475ca35338b4e9a131c9e2a8e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-data/zipball/d44e04839407bc32b029be59ba80090a5f720e91", - "reference": "d44e04839407bc32b029be59ba80090a5f720e91", + "url": "https://api.github.com/repos/spatie/laravel-data/zipball/df5b58baebae34475ca35338b4e9a131c9e2a8e0", + "reference": "df5b58baebae34475ca35338b4e9a131c9e2a8e0", "shasum": "" }, "require": { - "illuminate/contracts": "^9.30|^10.0|^11.0", + "illuminate/contracts": "^10.0|^11.0", "php": "^8.1", - "phpdocumentor/type-resolver": "^1.5", + "phpdocumentor/reflection": "^6.0", "spatie/laravel-package-tools": "^1.9.0", "spatie/php-structure-discoverer": "^2.0" }, "require-dev": { "fakerphp/faker": "^1.14", "friendsofphp/php-cs-fixer": "^3.0", - "inertiajs/inertia-laravel": "^0.6.3", + "inertiajs/inertia-laravel": "^1.2", + "livewire/livewire": "^3.0", "mockery/mockery": "^1.6", "nesbot/carbon": "^2.63", - "nette/php-generator": "^3.5", "nunomaduro/larastan": "^2.0", - "orchestra/testbench": "^7.6|^8.0", - "pestphp/pest": "^1.22", - "pestphp/pest-plugin-laravel": "^1.3", + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^2.31", + "pestphp/pest-plugin-laravel": "^2.0", + "pestphp/pest-plugin-livewire": "^2.1", "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.1", - "phpunit/phpunit": "^9.3", + "phpunit/phpunit": "^10.0", "spatie/invade": "^1.0", - "spatie/laravel-typescript-transformer": "^2.1.6", - "spatie/pest-plugin-snapshots": "^1.1", - "spatie/phpunit-snapshot-assertions": "^4.2", + "spatie/laravel-typescript-transformer": "^2.5", + "spatie/pest-plugin-snapshots": "^2.1", "spatie/test-time": "^1.2" }, "type": "library", @@ -7924,8 +7934,7 @@ }, "autoload": { "psr-4": { - "Spatie\\LaravelData\\": "src", - "Spatie\\LaravelData\\Database\\Factories\\": "database/factories" + "Spatie\\LaravelData\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -7948,7 +7957,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-data/issues", - "source": "https://github.com/spatie/laravel-data/tree/3.12.0" + "source": "https://github.com/spatie/laravel-data/tree/4.11.1" }, "funding": [ { @@ -7956,7 +7965,7 @@ "type": "github" } ], - "time": "2024-04-24T09:27:45+00:00" + "time": "2024-10-23T07:14:53+00:00" }, { "name": "spatie/laravel-package-tools", @@ -8460,16 +8469,16 @@ }, { "name": "stripe/stripe-php", - "version": "v12.8.0", + "version": "v16.2.0", "source": { "type": "git", "url": "https://github.com/stripe/stripe-php.git", - "reference": "6b6f4a775ad46fee4b1df2df4fdfa574365b1621" + "reference": "813ae4961755af28a13bda451689f7a6ed6498cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stripe/stripe-php/zipball/6b6f4a775ad46fee4b1df2df4fdfa574365b1621", - "reference": "6b6f4a775ad46fee4b1df2df4fdfa574365b1621", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/813ae4961755af28a13bda451689f7a6ed6498cb", + "reference": "813ae4961755af28a13bda451689f7a6ed6498cb", "shasum": "" }, "require": { @@ -8513,22 +8522,22 @@ ], "support": { "issues": "https://github.com/stripe/stripe-php/issues", - "source": "https://github.com/stripe/stripe-php/tree/v12.8.0" + "source": "https://github.com/stripe/stripe-php/tree/v16.2.0" }, - "time": "2023-10-16T18:04:12+00:00" + "time": "2024-10-29T21:15:53+00:00" }, { "name": "symfony/clock", - "version": "v7.1.1", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7" + "reference": "97bebc53548684c17ed696bc8af016880f0f098d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/3dfc8b084853586de51dd1441c6242c76a28cbe7", - "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7", + "url": "https://api.github.com/repos/symfony/clock/zipball/97bebc53548684c17ed696bc8af016880f0f098d", + "reference": "97bebc53548684c17ed696bc8af016880f0f098d", "shasum": "" }, "require": { @@ -8573,7 +8582,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.1.1" + "source": "https://github.com/symfony/clock/tree/v7.1.6" }, "funding": [ { @@ -8589,20 +8598,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/console", - "version": "v7.1.5", + "version": "v7.1.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee" + "reference": "ff04e5b5ba043d2badfb308197b9e6b42883fcd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee", - "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee", + "url": "https://api.github.com/repos/symfony/console/zipball/ff04e5b5ba043d2badfb308197b9e6b42883fcd5", + "reference": "ff04e5b5ba043d2badfb308197b9e6b42883fcd5", "shasum": "" }, "require": { @@ -8666,7 +8675,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.5" + "source": "https://github.com/symfony/console/tree/v7.1.8" }, "funding": [ { @@ -8682,20 +8691,20 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:28:38+00:00" + "time": "2024-11-06T14:23:19+00:00" }, { "name": "symfony/css-selector", - "version": "v7.1.1", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "1c7cee86c6f812896af54434f8ce29c8d94f9ff4" + "reference": "4aa4f6b3d6749c14d3aa815eef8226632e7bbc66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/1c7cee86c6f812896af54434f8ce29c8d94f9ff4", - "reference": "1c7cee86c6f812896af54434f8ce29c8d94f9ff4", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/4aa4f6b3d6749c14d3aa815eef8226632e7bbc66", + "reference": "4aa4f6b3d6749c14d3aa815eef8226632e7bbc66", "shasum": "" }, "require": { @@ -8731,7 +8740,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.1.1" + "source": "https://github.com/symfony/css-selector/tree/v7.1.6" }, "funding": [ { @@ -8747,7 +8756,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/deprecation-contracts", @@ -8818,16 +8827,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.1.3", + "version": "v7.1.7", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "432bb369952795c61ca1def65e078c4a80dad13c" + "reference": "010e44661f4c6babaf8c4862fe68c24a53903342" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/432bb369952795c61ca1def65e078c4a80dad13c", - "reference": "432bb369952795c61ca1def65e078c4a80dad13c", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/010e44661f4c6babaf8c4862fe68c24a53903342", + "reference": "010e44661f4c6babaf8c4862fe68c24a53903342", "shasum": "" }, "require": { @@ -8873,7 +8882,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.1.3" + "source": "https://github.com/symfony/error-handler/tree/v7.1.7" }, "funding": [ { @@ -8889,20 +8898,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T13:02:51+00:00" + "time": "2024-11-05T15:34:55+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.1.1", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7" + "reference": "87254c78dd50721cfd015b62277a8281c5589702" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", - "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/87254c78dd50721cfd015b62277a8281c5589702", + "reference": "87254c78dd50721cfd015b62277a8281c5589702", "shasum": "" }, "require": { @@ -8953,7 +8962,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.1.1" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.1.6" }, "funding": [ { @@ -8969,7 +8978,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -9049,16 +9058,16 @@ }, { "name": "symfony/finder", - "version": "v7.1.4", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "d95bbf319f7d052082fb7af147e0f835a695e823" + "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/d95bbf319f7d052082fb7af147e0f835a695e823", - "reference": "d95bbf319f7d052082fb7af147e0f835a695e823", + "url": "https://api.github.com/repos/symfony/finder/zipball/2cb89664897be33f78c65d3d2845954c8d7a43b8", + "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8", "shasum": "" }, "require": { @@ -9093,7 +9102,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.1.4" + "source": "https://github.com/symfony/finder/tree/v7.1.6" }, "funding": [ { @@ -9109,20 +9118,20 @@ "type": "tidelift" } ], - "time": "2024-08-13T14:28:19+00:00" + "time": "2024-10-01T08:31:23+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.1.5", + "version": "v7.1.8", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b" + "reference": "f4419ec69ccfc3f725a4de7c20e4e57626d10112" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e30ef73b1e44eea7eb37ba69600a354e553f694b", - "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f4419ec69ccfc3f725a4de7c20e4e57626d10112", + "reference": "f4419ec69ccfc3f725a4de7c20e4e57626d10112", "shasum": "" }, "require": { @@ -9132,12 +9141,12 @@ }, "conflict": { "doctrine/dbal": "<3.6", - "symfony/cache": "<6.4" + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" }, "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4|^7.0", + "symfony/cache": "^6.4.12|^7.1.5", "symfony/dependency-injection": "^6.4|^7.0", "symfony/expression-language": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", @@ -9170,7 +9179,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.1.5" + "source": "https://github.com/symfony/http-foundation/tree/v7.1.8" }, "funding": [ { @@ -9186,20 +9195,20 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:28:38+00:00" + "time": "2024-11-09T09:16:45+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.1.5", + "version": "v7.1.8", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "44204d96150a9df1fc57601ec933d23fefc2d65b" + "reference": "33fef24e3dc79d6d30bf4936531f2f4bd2ca189e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/44204d96150a9df1fc57601ec933d23fefc2d65b", - "reference": "44204d96150a9df1fc57601ec933d23fefc2d65b", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/33fef24e3dc79d6d30bf4936531f2f4bd2ca189e", + "reference": "33fef24e3dc79d6d30bf4936531f2f4bd2ca189e", "shasum": "" }, "require": { @@ -9284,7 +9293,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.1.5" + "source": "https://github.com/symfony/http-kernel/tree/v7.1.8" }, "funding": [ { @@ -9300,20 +9309,20 @@ "type": "tidelift" } ], - "time": "2024-09-21T06:09:21+00:00" + "time": "2024-11-13T14:25:32+00:00" }, { "name": "symfony/mailer", - "version": "v7.1.5", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "bbf21460c56f29810da3df3e206e38dfbb01e80b" + "reference": "69c9948451fb3a6a4d47dc8261d1794734e76cdd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/bbf21460c56f29810da3df3e206e38dfbb01e80b", - "reference": "bbf21460c56f29810da3df3e206e38dfbb01e80b", + "url": "https://api.github.com/repos/symfony/mailer/zipball/69c9948451fb3a6a4d47dc8261d1794734e76cdd", + "reference": "69c9948451fb3a6a4d47dc8261d1794734e76cdd", "shasum": "" }, "require": { @@ -9364,7 +9373,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.1.5" + "source": "https://github.com/symfony/mailer/tree/v7.1.6" }, "funding": [ { @@ -9380,20 +9389,20 @@ "type": "tidelift" } ], - "time": "2024-09-08T12:32:26+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/mime", - "version": "v7.1.5", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff" + "reference": "caa1e521edb2650b8470918dfe51708c237f0598" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/711d2e167e8ce65b05aea6b258c449671cdd38ff", - "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff", + "url": "https://api.github.com/repos/symfony/mime/zipball/caa1e521edb2650b8470918dfe51708c237f0598", + "reference": "caa1e521edb2650b8470918dfe51708c237f0598", "shasum": "" }, "require": { @@ -9448,7 +9457,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.1.5" + "source": "https://github.com/symfony/mime/tree/v7.1.6" }, "funding": [ { @@ -9464,20 +9473,20 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:28:38+00:00" + "time": "2024-10-25T15:11:02+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.1.1", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55" + "reference": "85e95eeede2d41cd146146e98c9c81d9214cae85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55", - "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/85e95eeede2d41cd146146e98c9c81d9214cae85", + "reference": "85e95eeede2d41cd146146e98c9c81d9214cae85", "shasum": "" }, "require": { @@ -9515,7 +9524,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.1.1" + "source": "https://github.com/symfony/options-resolver/tree/v7.1.6" }, "funding": [ { @@ -9531,7 +9540,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/polyfill-ctype", @@ -10251,16 +10260,16 @@ }, { "name": "symfony/process", - "version": "v7.1.5", + "version": "v7.1.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "5c03ee6369281177f07f7c68252a280beccba847" + "reference": "42783370fda6e538771f7c7a36e9fa2ee3a84892" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847", - "reference": "5c03ee6369281177f07f7c68252a280beccba847", + "url": "https://api.github.com/repos/symfony/process/zipball/42783370fda6e538771f7c7a36e9fa2ee3a84892", + "reference": "42783370fda6e538771f7c7a36e9fa2ee3a84892", "shasum": "" }, "require": { @@ -10292,7 +10301,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.1.5" + "source": "https://github.com/symfony/process/tree/v7.1.8" }, "funding": [ { @@ -10308,20 +10317,20 @@ "type": "tidelift" } ], - "time": "2024-09-19T21:48:23+00:00" + "time": "2024-11-06T14:23:19+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v7.1.4", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "405a7bcd872f1563966f64be19f1362d94ce71ab" + "reference": "f16471bb19f6685b9ccf0a2c03c213840ae68cd6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/405a7bcd872f1563966f64be19f1362d94ce71ab", - "reference": "405a7bcd872f1563966f64be19f1362d94ce71ab", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/f16471bb19f6685b9ccf0a2c03c213840ae68cd6", + "reference": "f16471bb19f6685b9ccf0a2c03c213840ae68cd6", "shasum": "" }, "require": { @@ -10375,7 +10384,7 @@ "psr-7" ], "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.1.4" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.1.6" }, "funding": [ { @@ -10391,20 +10400,20 @@ "type": "tidelift" } ], - "time": "2024-08-15T22:48:53+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/routing", - "version": "v7.1.4", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "1500aee0094a3ce1c92626ed8cf3c2037e86f5a7" + "reference": "66a2c469f6c22d08603235c46a20007c0701ea0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/1500aee0094a3ce1c92626ed8cf3c2037e86f5a7", - "reference": "1500aee0094a3ce1c92626ed8cf3c2037e86f5a7", + "url": "https://api.github.com/repos/symfony/routing/zipball/66a2c469f6c22d08603235c46a20007c0701ea0a", + "reference": "66a2c469f6c22d08603235c46a20007c0701ea0a", "shasum": "" }, "require": { @@ -10456,7 +10465,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.1.4" + "source": "https://github.com/symfony/routing/tree/v7.1.6" }, "funding": [ { @@ -10472,7 +10481,7 @@ "type": "tidelift" } ], - "time": "2024-08-29T08:16:25+00:00" + "time": "2024-10-01T08:31:23+00:00" }, { "name": "symfony/service-contracts", @@ -10559,16 +10568,16 @@ }, { "name": "symfony/stopwatch", - "version": "v7.1.1", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d" + "reference": "8b4a434e6e7faf6adedffb48783a5c75409a1a05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d", - "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/8b4a434e6e7faf6adedffb48783a5c75409a1a05", + "reference": "8b4a434e6e7faf6adedffb48783a5c75409a1a05", "shasum": "" }, "require": { @@ -10601,7 +10610,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.1.1" + "source": "https://github.com/symfony/stopwatch/tree/v7.1.6" }, "funding": [ { @@ -10617,20 +10626,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/string", - "version": "v7.1.5", + "version": "v7.1.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" + "reference": "591ebd41565f356fcd8b090fe64dbb5878f50281" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", - "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", + "url": "https://api.github.com/repos/symfony/string/zipball/591ebd41565f356fcd8b090fe64dbb5878f50281", + "reference": "591ebd41565f356fcd8b090fe64dbb5878f50281", "shasum": "" }, "require": { @@ -10688,7 +10697,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.5" + "source": "https://github.com/symfony/string/tree/v7.1.8" }, "funding": [ { @@ -10704,20 +10713,20 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:28:38+00:00" + "time": "2024-11-13T13:31:21+00:00" }, { "name": "symfony/translation", - "version": "v7.1.5", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "235535e3f84f3dfbdbde0208ede6ca75c3a489ea" + "reference": "b9f72ab14efdb6b772f85041fa12f820dee8d55f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/235535e3f84f3dfbdbde0208ede6ca75c3a489ea", - "reference": "235535e3f84f3dfbdbde0208ede6ca75c3a489ea", + "url": "https://api.github.com/repos/symfony/translation/zipball/b9f72ab14efdb6b772f85041fa12f820dee8d55f", + "reference": "b9f72ab14efdb6b772f85041fa12f820dee8d55f", "shasum": "" }, "require": { @@ -10782,7 +10791,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.1.5" + "source": "https://github.com/symfony/translation/tree/v7.1.6" }, "funding": [ { @@ -10798,7 +10807,7 @@ "type": "tidelift" } ], - "time": "2024-09-16T06:30:38+00:00" + "time": "2024-09-28T12:35:13+00:00" }, { "name": "symfony/translation-contracts", @@ -10880,16 +10889,16 @@ }, { "name": "symfony/uid", - "version": "v7.1.5", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "8c7bb8acb933964055215d89f9a9871df0239317" + "reference": "65befb3bb2d503bbffbd08c815aa38b472999917" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/8c7bb8acb933964055215d89f9a9871df0239317", - "reference": "8c7bb8acb933964055215d89f9a9871df0239317", + "url": "https://api.github.com/repos/symfony/uid/zipball/65befb3bb2d503bbffbd08c815aa38b472999917", + "reference": "65befb3bb2d503bbffbd08c815aa38b472999917", "shasum": "" }, "require": { @@ -10934,7 +10943,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.1.5" + "source": "https://github.com/symfony/uid/tree/v7.1.6" }, "funding": [ { @@ -10950,20 +10959,20 @@ "type": "tidelift" } ], - "time": "2024-09-17T09:16:35+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.1.5", + "version": "v7.1.8", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "e20e03889539fd4e4211e14d2179226c513c010d" + "reference": "7bb01a47b1b00428d32b5e7b4d3b2d1aa58d3db8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e20e03889539fd4e4211e14d2179226c513c010d", - "reference": "e20e03889539fd4e4211e14d2179226c513c010d", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7bb01a47b1b00428d32b5e7b4d3b2d1aa58d3db8", + "reference": "7bb01a47b1b00428d32b5e7b4d3b2d1aa58d3db8", "shasum": "" }, "require": { @@ -11017,7 +11026,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.1.5" + "source": "https://github.com/symfony/var-dumper/tree/v7.1.8" }, "funding": [ { @@ -11033,32 +11042,31 @@ "type": "tidelift" } ], - "time": "2024-09-16T10:07:02+00:00" + "time": "2024-11-08T15:46:42+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.12", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "762ee56b2649659380e0ef4d592d807bc17b7971" + "reference": "3ced3f29e4f0d6bce2170ff26719f1fe9aacc671" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/762ee56b2649659380e0ef4d592d807bc17b7971", - "reference": "762ee56b2649659380e0ef4d592d807bc17b7971", + "url": "https://api.github.com/repos/symfony/yaml/zipball/3ced3f29e4f0d6bce2170ff26719f1fe9aacc671", + "reference": "3ced3f29e4f0d6bce2170ff26719f1fe9aacc671", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<5.4" + "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^5.4|^6.0|^7.0" + "symfony/console": "^6.4|^7.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -11089,7 +11097,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.12" + "source": "https://github.com/symfony/yaml/tree/v7.1.6" }, "funding": [ { @@ -11105,7 +11113,7 @@ "type": "tidelift" } ], - "time": "2024-09-17T12:47:12+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -11162,25 +11170,24 @@ }, { "name": "visus/cuid2", - "version": "2.0.0", + "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/visus-io/php-cuid2.git", - "reference": "907919cadd8dfeb24ffecf7209ec4988fb9b3fc0" + "reference": "17c9b3098d556bb2556a084c948211333cc19c79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/visus-io/php-cuid2/zipball/907919cadd8dfeb24ffecf7209ec4988fb9b3fc0", - "reference": "907919cadd8dfeb24ffecf7209ec4988fb9b3fc0", + "url": "https://api.github.com/repos/visus-io/php-cuid2/zipball/17c9b3098d556bb2556a084c948211333cc19c79", + "reference": "17c9b3098d556bb2556a084c948211333cc19c79", "shasum": "" }, "require": { - "php": "^8.0" + "php": "^8.1" }, "require-dev": { "ergebnis/composer-normalize": "^2.29", "ext-ctype": "*", - "php-parallel-lint/php-parallel-lint": "^1.3", "phpstan/phpstan": "^1.9", "phpunit/phpunit": "^10.0", "squizlabs/php_codesniffer": "^3.7", @@ -11200,7 +11207,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "MIT" ], "authors": [ { @@ -11215,9 +11222,9 @@ ], "support": { "issues": "https://github.com/visus-io/php-cuid2/issues", - "source": "https://github.com/visus-io/php-cuid2/tree/2.0.0" + "source": "https://github.com/visus-io/php-cuid2/tree/4.1.0" }, - "time": "2023-03-23T19:18:36+00:00" + "time": "2024-05-14T13:23:35+00:00" }, { "name": "vlucas/phpdotenv", @@ -11755,16 +11762,16 @@ }, { "name": "zircote/swagger-php", - "version": "4.11.0", + "version": "4.11.1", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "3b6f3800f4fd6544ada4dce180c6b69eaead7c7c" + "reference": "7df10e8ec47db07c031db317a25bef962b4e5de1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/3b6f3800f4fd6544ada4dce180c6b69eaead7c7c", - "reference": "3b6f3800f4fd6544ada4dce180c6b69eaead7c7c", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/7df10e8ec47db07c031db317a25bef962b4e5de1", + "reference": "7df10e8ec47db07c031db317a25bef962b4e5de1", "shasum": "" }, "require": { @@ -11830,24 +11837,24 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/4.11.0" + "source": "https://github.com/zircote/swagger-php/tree/4.11.1" }, - "time": "2024-10-09T03:11:12+00:00" + "time": "2024-10-15T19:20:02+00:00" } ], "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.14.3", + "version": "v3.14.7", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd" + "reference": "f484b8c9124de0b163da39958331098ffcd4a65e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd", - "reference": "c0bee7c08ae2429e4a9ed2bc75679b012db6e3bd", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/f484b8c9124de0b163da39958331098ffcd4a65e", + "reference": "f484b8c9124de0b163da39958331098ffcd4a65e", "shasum": "" }, "require": { @@ -11906,7 +11913,7 @@ ], "support": { "issues": "https://github.com/barryvdh/laravel-debugbar/issues", - "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.14.3" + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.14.7" }, "funding": [ { @@ -11918,20 +11925,20 @@ "type": "github" } ], - "time": "2024-10-02T09:17:49+00:00" + "time": "2024-11-14T09:12:35+00:00" }, { "name": "brianium/paratest", - "version": "v7.4.3", + "version": "v7.6.0", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "64fcfd0e28a6b8078a19dbf9127be2ee645b92ec" + "reference": "68ff89a8de47d086588e391a516d2a5b5fde6254" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/64fcfd0e28a6b8078a19dbf9127be2ee645b92ec", - "reference": "64fcfd0e28a6b8078a19dbf9127be2ee645b92ec", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/68ff89a8de47d086588e391a516d2a5b5fde6254", + "reference": "68ff89a8de47d086588e391a516d2a5b5fde6254", "shasum": "" }, "require": { @@ -11939,31 +11946,30 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-simplexml": "*", - "fidry/cpu-core-counter": "^1.1.0", - "jean85/pretty-package-versions": "^2.0.5", - "php": "~8.2.0 || ~8.3.0", - "phpunit/php-code-coverage": "^10.1.11 || ^11.0.0", - "phpunit/php-file-iterator": "^4.1.0 || ^5.0.0", - "phpunit/php-timer": "^6.0.0 || ^7.0.0", - "phpunit/phpunit": "^10.5.9 || ^11.0.3", - "sebastian/environment": "^6.0.1 || ^7.0.0", - "symfony/console": "^6.4.3 || ^7.0.3", - "symfony/process": "^6.4.3 || ^7.0.3" + "fidry/cpu-core-counter": "^1.2.0", + "jean85/pretty-package-versions": "^2.0.6", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "phpunit/php-code-coverage": "^11.0.7", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-timer": "^7.0.1", + "phpunit/phpunit": "^11.4.1", + "sebastian/environment": "^7.2.0", + "symfony/console": "^6.4.11 || ^7.1.5", + "symfony/process": "^6.4.8 || ^7.1.5" }, "require-dev": { "doctrine/coding-standard": "^12.0.0", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^1.10.58", - "phpstan/phpstan-deprecation-rules": "^1.1.4", - "phpstan/phpstan-phpunit": "^1.3.15", - "phpstan/phpstan-strict-rules": "^1.5.2", - "squizlabs/php_codesniffer": "^3.9.0", - "symfony/filesystem": "^6.4.3 || ^7.0.3" + "phpstan/phpstan": "^1.12.6", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.1", + "squizlabs/php_codesniffer": "^3.10.3", + "symfony/filesystem": "^6.4.9 || ^7.1.5" }, "bin": [ "bin/paratest", - "bin/paratest.bat", "bin/paratest_for_phpstorm" ], "type": "library", @@ -12000,7 +12006,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.4.3" + "source": "https://github.com/paratestphp/paratest/tree/v7.6.0" }, "funding": [ { @@ -12012,20 +12018,20 @@ "type": "paypal" } ], - "time": "2024-02-20T07:24:02+00:00" + "time": "2024-10-15T12:38:31+00:00" }, { "name": "fakerphp/faker", - "version": "v1.23.1", + "version": "v1.24.0", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b" + "reference": "a136842a532bac9ecd8a1c723852b09915d7db50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/bfb4fe148adbf78eff521199619b93a52ae3554b", - "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/a136842a532bac9ecd8a1c723852b09915d7db50", + "reference": "a136842a532bac9ecd8a1c723852b09915d7db50", "shasum": "" }, "require": { @@ -12073,9 +12079,9 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.23.1" + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.0" }, - "time": "2024-01-02T13:46:09+00:00" + "time": "2024-11-07T15:11:20+00:00" }, { "name": "fidry/cpu-core-counter", @@ -12262,16 +12268,16 @@ }, { "name": "laravel/dusk", - "version": "v8.2.8", + "version": "v8.2.11", "source": { "type": "git", "url": "https://github.com/laravel/dusk.git", - "reference": "5bff1e8dd87ec653a2202475377152e5d14fde40" + "reference": "c667db6d8795f0ccc8f63d54a7780ce8a0cc3d3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/dusk/zipball/5bff1e8dd87ec653a2202475377152e5d14fde40", - "reference": "5bff1e8dd87ec653a2202475377152e5d14fde40", + "url": "https://api.github.com/repos/laravel/dusk/zipball/c667db6d8795f0ccc8f63d54a7780ce8a0cc3d3c", + "reference": "c667db6d8795f0ccc8f63d54a7780ce8a0cc3d3c", "shasum": "" }, "require": { @@ -12328,9 +12334,9 @@ ], "support": { "issues": "https://github.com/laravel/dusk/issues", - "source": "https://github.com/laravel/dusk/tree/v8.2.8" + "source": "https://github.com/laravel/dusk/tree/v8.2.11" }, - "time": "2024-10-04T14:02:20+00:00" + "time": "2024-11-07T21:51:32+00:00" }, { "name": "laravel/pint", @@ -12400,16 +12406,16 @@ }, { "name": "laravel/telescope", - "version": "v5.2.4", + "version": "v5.2.5", "source": { "type": "git", "url": "https://github.com/laravel/telescope.git", - "reference": "749369e996611d803e7c1b57929b482dd676008d" + "reference": "f68386a8d816c9e3a011b8301bfd263213bf00d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/telescope/zipball/749369e996611d803e7c1b57929b482dd676008d", - "reference": "749369e996611d803e7c1b57929b482dd676008d", + "url": "https://api.github.com/repos/laravel/telescope/zipball/f68386a8d816c9e3a011b8301bfd263213bf00d4", + "reference": "f68386a8d816c9e3a011b8301bfd263213bf00d4", "shasum": "" }, "require": { @@ -12463,22 +12469,22 @@ ], "support": { "issues": "https://github.com/laravel/telescope/issues", - "source": "https://github.com/laravel/telescope/tree/v5.2.4" + "source": "https://github.com/laravel/telescope/tree/v5.2.5" }, - "time": "2024-10-29T15:35:13+00:00" + "time": "2024-10-31T17:06:07+00:00" }, { "name": "maximebf/debugbar", - "version": "v1.23.2", + "version": "v1.23.3", "source": { "type": "git", "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "689720d724c771ac4add859056744b7b3f2406da" + "reference": "687400043d77943ef95e8417cb44e1673ee57844" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/689720d724c771ac4add859056744b7b3f2406da", - "reference": "689720d724c771ac4add859056744b7b3f2406da", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/687400043d77943ef95e8417cb44e1673ee57844", + "reference": "687400043d77943ef95e8417cb44e1673ee57844", "shasum": "" }, "require": { @@ -12531,9 +12537,9 @@ ], "support": { "issues": "https://github.com/maximebf/php-debugbar/issues", - "source": "https://github.com/maximebf/php-debugbar/tree/v1.23.2" + "source": "https://github.com/maximebf/php-debugbar/tree/v1.23.3" }, - "time": "2024-09-16T11:23:09+00:00" + "time": "2024-10-29T12:24:25+00:00" }, { "name": "mockery/mockery", @@ -12620,16 +12626,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.12.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", "shasum": "" }, "require": { @@ -12668,7 +12674,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" }, "funding": [ { @@ -12676,27 +12682,27 @@ "type": "tidelift" } ], - "time": "2024-06-12T14:39:25+00:00" + "time": "2024-11-08T17:47:46+00:00" }, { "name": "nunomaduro/collision", - "version": "v8.4.0", + "version": "v8.5.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "e7d1aa8ed753f63fa816932bbc89678238843b4a" + "reference": "f5c101b929c958e849a633283adff296ed5f38f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/e7d1aa8ed753f63fa816932bbc89678238843b4a", - "reference": "e7d1aa8ed753f63fa816932bbc89678238843b4a", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/f5c101b929c958e849a633283adff296ed5f38f5", + "reference": "f5c101b929c958e849a633283adff296ed5f38f5", "shasum": "" }, "require": { - "filp/whoops": "^2.15.4", - "nunomaduro/termwind": "^2.0.1", + "filp/whoops": "^2.16.0", + "nunomaduro/termwind": "^2.1.0", "php": "^8.2.0", - "symfony/console": "^7.1.3" + "symfony/console": "^7.1.5" }, "conflict": { "laravel/framework": "<11.0.0 || >=12.0.0", @@ -12704,14 +12710,14 @@ }, "require-dev": { "larastan/larastan": "^2.9.8", - "laravel/framework": "^11.19.0", - "laravel/pint": "^1.17.1", - "laravel/sail": "^1.31.0", - "laravel/sanctum": "^4.0.2", - "laravel/tinker": "^2.9.0", - "orchestra/testbench-core": "^9.2.3", - "pestphp/pest": "^2.35.0 || ^3.0.0", - "sebastian/environment": "^6.1.0 || ^7.0.0" + "laravel/framework": "^11.28.0", + "laravel/pint": "^1.18.1", + "laravel/sail": "^1.36.0", + "laravel/sanctum": "^4.0.3", + "laravel/tinker": "^2.10.0", + "orchestra/testbench-core": "^9.5.3", + "pestphp/pest": "^2.36.0 || ^3.4.0", + "sebastian/environment": "^6.1.0 || ^7.2.0" }, "type": "library", "extra": { @@ -12773,40 +12779,42 @@ "type": "patreon" } ], - "time": "2024-08-03T15:32:23+00:00" + "time": "2024-10-15T16:06:32+00:00" }, { "name": "pestphp/pest", - "version": "v2.35.1", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "b13acb630df52c06123588d321823c31fc685545" + "reference": "179d46ce97d52bcb3f791449ae94025c3f32e3e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/b13acb630df52c06123588d321823c31fc685545", - "reference": "b13acb630df52c06123588d321823c31fc685545", + "url": "https://api.github.com/repos/pestphp/pest/zipball/179d46ce97d52bcb3f791449ae94025c3f32e3e3", + "reference": "179d46ce97d52bcb3f791449ae94025c3f32e3e3", "shasum": "" }, "require": { - "brianium/paratest": "^7.3.1", - "nunomaduro/collision": "^7.10.0|^8.4.0", - "nunomaduro/termwind": "^1.15.1|^2.0.1", - "pestphp/pest-plugin": "^2.1.1", - "pestphp/pest-plugin-arch": "^2.7.0", - "php": "^8.1.0", - "phpunit/phpunit": "^10.5.17" + "brianium/paratest": "^7.6.0", + "nunomaduro/collision": "^8.5.0", + "nunomaduro/termwind": "^2.2.0", + "pestphp/pest-plugin": "^3.0.0", + "pestphp/pest-plugin-arch": "^3.0.0", + "pestphp/pest-plugin-mutate": "^3.0.5", + "php": "^8.2.0", + "phpunit/phpunit": "^11.4.3" }, "conflict": { - "phpunit/phpunit": ">10.5.17", - "sebastian/exporter": "<5.1.0", + "filp/whoops": "<2.16.0", + "phpunit/phpunit": ">11.4.3", + "sebastian/exporter": "<6.0.0", "webmozart/assert": "<1.11.0" }, "require-dev": { - "pestphp/pest-dev-tools": "^2.16.0", - "pestphp/pest-plugin-type-coverage": "^2.8.5", - "symfony/process": "^6.4.0|^7.1.3" + "pestphp/pest-dev-tools": "^3.3.0", + "pestphp/pest-plugin-type-coverage": "^3.1.0", + "symfony/process": "^7.1.6" }, "bin": [ "bin/pest" @@ -12815,6 +12823,8 @@ "extra": { "pest": { "plugins": [ + "Pest\\Mutate\\Plugins\\Mutate", + "Pest\\Plugins\\Configuration", "Pest\\Plugins\\Bail", "Pest\\Plugins\\Cache", "Pest\\Plugins\\Coverage", @@ -12869,7 +12879,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v2.35.1" + "source": "https://github.com/pestphp/pest/tree/v3.5.1" }, "funding": [ { @@ -12881,34 +12891,34 @@ "type": "github" } ], - "time": "2024-08-20T21:41:50+00:00" + "time": "2024-10-31T16:12:45+00:00" }, { "name": "pestphp/pest-plugin", - "version": "v2.1.1", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin.git", - "reference": "e05d2859e08c2567ee38ce8b005d044e72648c0b" + "reference": "e79b26c65bc11c41093b10150c1341cc5cdbea83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/e05d2859e08c2567ee38ce8b005d044e72648c0b", - "reference": "e05d2859e08c2567ee38ce8b005d044e72648c0b", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/e79b26c65bc11c41093b10150c1341cc5cdbea83", + "reference": "e79b26c65bc11c41093b10150c1341cc5cdbea83", "shasum": "" }, "require": { "composer-plugin-api": "^2.0.0", "composer-runtime-api": "^2.2.2", - "php": "^8.1" + "php": "^8.2" }, "conflict": { - "pestphp/pest": "<2.2.3" + "pestphp/pest": "<3.0.0" }, "require-dev": { - "composer/composer": "^2.5.8", - "pestphp/pest": "^2.16.0", - "pestphp/pest-dev-tools": "^2.16.0" + "composer/composer": "^2.7.9", + "pestphp/pest": "^3.0.0", + "pestphp/pest-dev-tools": "^3.0.0" }, "type": "composer-plugin", "extra": { @@ -12935,7 +12945,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin/tree/v2.1.1" + "source": "https://github.com/pestphp/pest-plugin/tree/v3.0.0" }, "funding": [ { @@ -12951,31 +12961,30 @@ "type": "patreon" } ], - "time": "2023-08-22T08:40:06+00:00" + "time": "2024-09-08T23:21:41+00:00" }, { "name": "pestphp/pest-plugin-arch", - "version": "v2.7.0", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-arch.git", - "reference": "d23b2d7498475354522c3818c42ef355dca3fcda" + "reference": "0a27e55a270cfe73d8cb70551b91002ee2cb64b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/d23b2d7498475354522c3818c42ef355dca3fcda", - "reference": "d23b2d7498475354522c3818c42ef355dca3fcda", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/0a27e55a270cfe73d8cb70551b91002ee2cb64b0", + "reference": "0a27e55a270cfe73d8cb70551b91002ee2cb64b0", "shasum": "" }, "require": { - "nunomaduro/collision": "^7.10.0|^8.1.0", - "pestphp/pest-plugin": "^2.1.1", - "php": "^8.1", + "pestphp/pest-plugin": "^3.0.0", + "php": "^8.2", "ta-tikoma/phpunit-architecture-test": "^0.8.4" }, "require-dev": { - "pestphp/pest": "^2.33.0", - "pestphp/pest-dev-tools": "^2.16.0" + "pestphp/pest": "^3.0.0", + "pestphp/pest-dev-tools": "^3.0.0" }, "type": "library", "extra": { @@ -13010,7 +13019,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-arch/tree/v2.7.0" + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v3.0.0" }, "funding": [ { @@ -13022,7 +13031,79 @@ "type": "github" } ], - "time": "2024-01-26T09:46:42+00:00" + "time": "2024-09-08T23:23:55+00:00" + }, + { + "name": "pestphp/pest-plugin-mutate", + "version": "v3.0.5", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-mutate.git", + "reference": "e10dbdc98c9e2f3890095b4fe2144f63a5717e08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/e10dbdc98c9e2f3890095b4fe2144f63a5717e08", + "reference": "e10dbdc98c9e2f3890095b4fe2144f63a5717e08", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.2.0", + "pestphp/pest-plugin": "^3.0.0", + "php": "^8.2", + "psr/simple-cache": "^3.0.0" + }, + "require-dev": { + "pestphp/pest": "^3.0.8", + "pestphp/pest-dev-tools": "^3.0.0", + "pestphp/pest-plugin-type-coverage": "^3.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pest\\Mutate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" + } + ], + "description": "Mutates your code to find untested cases", + "keywords": [ + "framework", + "mutate", + "mutation", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v3.0.5" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2024-09-22T07:54:40+00:00" }, { "name": "phar-io/manifest", @@ -13208,101 +13289,37 @@ }, "time": "2023-10-20T12:21:20+00:00" }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.4.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", - "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", - "shasum": "" - }, - "require": { - "doctrine/deprecations": "^1.1", - "ext-filter": "*", - "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.7", - "phpstan/phpdoc-parser": "^1.7", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.5", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-webmozart-assert": "^1.2", - "phpunit/phpunit": "^9.5", - "vimeo/psalm": "^5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.1" - }, - "time": "2024-05-21T05:55:05+00:00" - }, { "name": "phpunit/php-code-coverage", - "version": "10.1.16", + "version": "11.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + "reference": "f7f08030e8811582cc459871d28d6f5a1a4d35ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", - "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f7f08030e8811582cc459871d28d6f5a1a4d35ca", + "reference": "f7f08030e8811582cc459871d28d6f5a1a4d35ca", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.19.1 || ^5.1.0", - "php": ">=8.1", - "phpunit/php-file-iterator": "^4.1.0", - "phpunit/php-text-template": "^3.0.1", - "sebastian/code-unit-reverse-lookup": "^3.0.0", - "sebastian/complexity": "^3.2.0", - "sebastian/environment": "^6.1.0", - "sebastian/lines-of-code": "^2.0.2", - "sebastian/version": "^4.0.1", + "nikic/php-parser": "^5.3.1", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^10.1" + "phpunit/phpunit": "^11.4.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -13311,7 +13328,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.1.x-dev" + "dev-main": "11.0.x-dev" } }, "autoload": { @@ -13340,7 +13357,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.7" }, "funding": [ { @@ -13348,32 +13365,32 @@ "type": "github" } ], - "time": "2024-08-22T04:31:57+00:00" + "time": "2024-10-09T06:21:38+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "4.1.0", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", - "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -13401,7 +13418,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" }, "funding": [ { @@ -13409,28 +13426,28 @@ "type": "github" } ], - "time": "2023-08-31T06:24:48+00:00" + "time": "2024-08-27T05:02:59+00:00" }, { "name": "phpunit/php-invoker", - "version": "4.0.0", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", - "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "suggest": { "ext-pcntl": "*" @@ -13438,7 +13455,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -13464,7 +13481,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" }, "funding": [ { @@ -13472,32 +13490,32 @@ "type": "github" } ], - "time": "2023-02-03T06:56:09+00:00" + "time": "2024-07-03T05:07:44+00:00" }, { "name": "phpunit/php-text-template", - "version": "3.0.1", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", - "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -13524,7 +13542,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" }, "funding": [ { @@ -13532,32 +13550,32 @@ "type": "github" } ], - "time": "2023-08-31T14:07:24+00:00" + "time": "2024-07-03T05:08:43+00:00" }, { "name": "phpunit/php-timer", - "version": "6.0.0", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", - "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -13583,7 +13601,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" }, "funding": [ { @@ -13591,20 +13610,20 @@ "type": "github" } ], - "time": "2023-02-03T06:57:52+00:00" + "time": "2024-07-03T05:09:35+00:00" }, { "name": "phpunit/phpunit", - "version": "10.5.17", + "version": "11.4.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c1f736a473d21957ead7e94fcc029f571895abf5" + "reference": "e8e8ed1854de5d36c088ec1833beae40d2dedd76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c1f736a473d21957ead7e94fcc029f571895abf5", - "reference": "c1f736a473d21957ead7e94fcc029f571895abf5", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e8e8ed1854de5d36c088ec1833beae40d2dedd76", + "reference": "e8e8ed1854de5d36c088ec1833beae40d2dedd76", "shasum": "" }, "require": { @@ -13614,26 +13633,25 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", - "php": ">=8.1", - "phpunit/php-code-coverage": "^10.1.5", - "phpunit/php-file-iterator": "^4.0", - "phpunit/php-invoker": "^4.0", - "phpunit/php-text-template": "^3.0", - "phpunit/php-timer": "^6.0", - "sebastian/cli-parser": "^2.0", - "sebastian/code-unit": "^2.0", - "sebastian/comparator": "^5.0", - "sebastian/diff": "^5.0", - "sebastian/environment": "^6.0", - "sebastian/exporter": "^5.1", - "sebastian/global-state": "^6.0.1", - "sebastian/object-enumerator": "^5.0", - "sebastian/recursion-context": "^5.0", - "sebastian/type": "^4.0", - "sebastian/version": "^4.0" + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.7", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.1", + "sebastian/comparator": "^6.1.1", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.0", + "sebastian/exporter": "^6.1.3", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.1.0", + "sebastian/version": "^5.0.2" }, "suggest": { "ext-soap": "To be able to generate mocks based on WSDL files" @@ -13644,7 +13662,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.5-dev" + "dev-main": "11.4-dev" } }, "autoload": { @@ -13676,7 +13694,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.17" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.4.3" }, "funding": [ { @@ -13692,32 +13710,32 @@ "type": "tidelift" } ], - "time": "2024-04-05T04:39:01+00:00" + "time": "2024-10-28T13:07:50+00:00" }, { "name": "sebastian/cli-parser", - "version": "2.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", - "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -13741,7 +13759,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" }, "funding": [ { @@ -13749,32 +13767,32 @@ "type": "github" } ], - "time": "2024-03-02T07:12:49+00:00" + "time": "2024-07-03T04:41:36+00:00" }, { "name": "sebastian/code-unit", - "version": "2.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + "reference": "6bb7d09d6623567178cf54126afa9c2310114268" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", - "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/6bb7d09d6623567178cf54126afa9c2310114268", + "reference": "6bb7d09d6623567178cf54126afa9c2310114268", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -13797,7 +13815,8 @@ "homepage": "https://github.com/sebastianbergmann/code-unit", "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.1" }, "funding": [ { @@ -13805,32 +13824,32 @@ "type": "github" } ], - "time": "2023-02-03T06:58:43+00:00" + "time": "2024-07-03T04:44:28+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "3.0.0", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", - "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -13852,7 +13871,8 @@ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" }, "funding": [ { @@ -13860,36 +13880,36 @@ "type": "github" } ], - "time": "2023-02-03T06:59:15+00:00" + "time": "2024-07-03T04:45:54+00:00" }, { "name": "sebastian/comparator", - "version": "5.0.2", + "version": "6.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2d3e04c3b4c1e84a5e7382221ad8883c8fbc4f53" + "reference": "43d129d6a0f81c78bee378b46688293eb7ea3739" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2d3e04c3b4c1e84a5e7382221ad8883c8fbc4f53", - "reference": "2d3e04c3b4c1e84a5e7382221ad8883c8fbc4f53", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/43d129d6a0f81c78bee378b46688293eb7ea3739", + "reference": "43d129d6a0f81c78bee378b46688293eb7ea3739", "shasum": "" }, "require": { "ext-dom": "*", "ext-mbstring": "*", - "php": ">=8.1", - "sebastian/diff": "^5.0", - "sebastian/exporter": "^5.0" + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^10.4" + "phpunit/phpunit": "^11.4" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.2-dev" } }, "autoload": { @@ -13929,7 +13949,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.2" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.2.1" }, "funding": [ { @@ -13937,33 +13957,33 @@ "type": "github" } ], - "time": "2024-08-12T06:03:08+00:00" + "time": "2024-10-31T05:30:08+00:00" }, { "name": "sebastian/complexity", - "version": "3.2.0", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "68ff824baeae169ec9f2137158ee529584553799" + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", - "reference": "68ff824baeae169ec9f2137158ee529584553799", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=8.1" + "nikic/php-parser": "^5.0", + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.2-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -13987,7 +14007,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" }, "funding": [ { @@ -13995,33 +14015,33 @@ "type": "github" } ], - "time": "2023-12-21T08:37:17+00:00" + "time": "2024-07-03T04:49:50+00:00" }, { "name": "sebastian/diff", - "version": "5.1.1", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", - "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0", - "symfony/process": "^6.4" + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -14054,7 +14074,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" }, "funding": [ { @@ -14062,27 +14082,27 @@ "type": "github" } ], - "time": "2024-03-02T07:15:17+00:00" + "time": "2024-07-03T04:53:05+00:00" }, { "name": "sebastian/environment", - "version": "6.1.0", + "version": "7.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", - "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "suggest": { "ext-posix": "*" @@ -14090,7 +14110,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.1-dev" + "dev-main": "7.2-dev" } }, "autoload": { @@ -14118,7 +14138,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" }, "funding": [ { @@ -14126,34 +14146,34 @@ "type": "github" } ], - "time": "2024-03-23T08:47:14+00:00" + "time": "2024-07-03T04:54:44+00:00" }, { "name": "sebastian/exporter", - "version": "5.1.2", + "version": "6.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "955288482d97c19a372d3f31006ab3f37da47adf" + "reference": "c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", - "reference": "955288482d97c19a372d3f31006ab3f37da47adf", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e", + "reference": "c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": ">=8.1", - "sebastian/recursion-context": "^5.0" + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.2" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -14196,7 +14216,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" + "source": "https://github.com/sebastianbergmann/exporter/tree/6.1.3" }, "funding": [ { @@ -14204,35 +14224,35 @@ "type": "github" } ], - "time": "2024-03-02T07:17:12+00:00" + "time": "2024-07-03T04:56:19+00:00" }, { "name": "sebastian/global-state", - "version": "6.0.2", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", - "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", "shasum": "" }, "require": { - "php": ">=8.1", - "sebastian/object-reflector": "^3.0", - "sebastian/recursion-context": "^5.0" + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -14258,7 +14278,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" }, "funding": [ { @@ -14266,33 +14286,33 @@ "type": "github" } ], - "time": "2024-03-02T07:19:19+00:00" + "time": "2024-07-03T04:57:36+00:00" }, { "name": "sebastian/lines-of-code", - "version": "2.0.2", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", - "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=8.1" + "nikic/php-parser": "^5.0", + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -14316,7 +14336,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" }, "funding": [ { @@ -14324,34 +14344,34 @@ "type": "github" } ], - "time": "2023-12-21T08:38:20+00:00" + "time": "2024-07-03T04:58:38+00:00" }, { "name": "sebastian/object-enumerator", - "version": "5.0.0", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", - "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", "shasum": "" }, "require": { - "php": ">=8.1", - "sebastian/object-reflector": "^3.0", - "sebastian/recursion-context": "^5.0" + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -14373,7 +14393,8 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" }, "funding": [ { @@ -14381,32 +14402,32 @@ "type": "github" } ], - "time": "2023-02-03T07:08:32+00:00" + "time": "2024-07-03T05:00:13+00:00" }, { "name": "sebastian/object-reflector", - "version": "3.0.0", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", - "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -14428,7 +14449,8 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" }, "funding": [ { @@ -14436,32 +14458,32 @@ "type": "github" } ], - "time": "2023-02-03T07:06:18+00:00" + "time": "2024-07-03T05:01:32+00:00" }, { "name": "sebastian/recursion-context", - "version": "5.0.0", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", - "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -14491,7 +14513,8 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" }, "funding": [ { @@ -14499,32 +14522,32 @@ "type": "github" } ], - "time": "2023-02-03T07:05:40+00:00" + "time": "2024-07-03T05:10:34+00:00" }, { "name": "sebastian/type", - "version": "4.0.0", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", - "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac", + "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -14547,7 +14570,8 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.0" }, "funding": [ { @@ -14555,29 +14579,29 @@ "type": "github" } ], - "time": "2023-02-03T07:10:45+00:00" + "time": "2024-09-17T13:12:04+00:00" }, { "name": "sebastian/version", - "version": "4.0.1", + "version": "5.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", - "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -14600,7 +14624,8 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" }, "funding": [ { @@ -14608,20 +14633,20 @@ "type": "github" } ], - "time": "2023-02-07T11:34:05+00:00" + "time": "2024-10-09T05:16:32+00:00" }, { "name": "serversideup/spin", - "version": "v1.1.0", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/serversideup/spin.git", - "reference": "03bb69dbdc6d6a68b82b4bb4cfeb7accc4f8758f" + "reference": "e7f742dfe54146196da26876670f368c11852df3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/serversideup/spin/zipball/03bb69dbdc6d6a68b82b4bb4cfeb7accc4f8758f", - "reference": "03bb69dbdc6d6a68b82b4bb4cfeb7accc4f8758f", + "url": "https://api.github.com/repos/serversideup/spin/zipball/e7f742dfe54146196da26876670f368c11852df3", + "reference": "e7f742dfe54146196da26876670f368c11852df3", "shasum": "" }, "bin": [ @@ -14645,7 +14670,7 @@ "description": "Replicate your production environment locally using Docker. Just run \"spin up\". It's really that easy.", "support": { "issues": "https://github.com/serversideup/spin/issues", - "source": "https://github.com/serversideup/spin/tree/v1.1.0" + "source": "https://github.com/serversideup/spin/tree/v2.3.0" }, "funding": [ { @@ -14653,7 +14678,7 @@ "type": "github" } ], - "time": "2022-05-20T15:13:10+00:00" + "time": "2024-10-15T15:12:28+00:00" }, { "name": "spatie/error-solutions", @@ -14974,20 +14999,20 @@ }, { "name": "symfony/http-client", - "version": "v6.4.12", + "version": "v7.1.8", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56" + "reference": "c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", - "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", + "url": "https://api.github.com/repos/symfony/http-client/zipball/c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a", + "reference": "c30d91a1deac0dc3ed5e604683cf2e1dfc635b8a", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-client-contracts": "^3.4.1", @@ -14995,7 +15020,7 @@ }, "conflict": { "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.3" + "symfony/http-foundation": "<6.4" }, "provide": { "php-http/async-client-implementation": "*", @@ -15012,11 +15037,12 @@ "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0" + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -15047,7 +15073,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.12" + "source": "https://github.com/symfony/http-client/tree/v7.1.8" }, "funding": [ { @@ -15063,7 +15089,7 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:21:33+00:00" + "time": "2024-11-13T13:40:27+00:00" }, { "name": "symfony/http-client-contracts", diff --git a/config/constants.php b/config/constants.php index 5792b358c..f7d5a7831 100644 --- a/config/constants.php +++ b/config/constants.php @@ -1,10 +1,47 @@ [ - 'base_url' => 'https://coolify.io/docs', + 'coolify' => [ + 'version' => '4.0.0-beta.376', + 'self_hosted' => env('SELF_HOSTED', true), + 'autoupdate' => env('AUTOUPDATE'), + 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), + 'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper'), + 'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false), + ], + + 'urls' => [ + 'docs' => 'https://coolify.io/docs', 'contact' => 'https://coolify.io/docs/contact', ], + + 'services' => [ + // Temporary disabled until cache is implemented + // 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json', + 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json', + ], + + 'terminal' => [ + 'protocol' => env('TERMINAL_PROTOCOL'), + 'host' => env('TERMINAL_HOST'), + 'port' => env('TERMINAL_PORT'), + ], + + 'pusher' => [ + 'host' => env('PUSHER_HOST'), + 'port' => env('PUSHER_PORT'), + 'app_key' => env('PUSHER_APP_KEY'), + ], + + 'horizon' => [ + 'is_horizon_enabled' => env('HORIZON_ENABLED', true), + 'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true), + ], + + 'docker' => [ + 'minimum_required_version' => '26.0', + ], + 'ssh' => [ 'mux_enabled' => env('MUX_ENABLED', env('SSH_MUX_ENABLED', true)), 'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', 3600), @@ -12,20 +49,14 @@ return [ 'server_interval' => 20, 'command_timeout' => 7200, ], - 'waitlist' => [ - 'expiration' => 10, - ], + 'invitation' => [ 'link' => [ 'base_url' => '/invitations/', - 'expiration' => 10, + 'expiration_days' => 3, ], ], - 'services' => [ - // Temporary disabled until cache is implemented - // 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json', - 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json', - ], + 'limits' => [ 'trial_period' => 0, 'server' => [ @@ -45,4 +76,23 @@ return [ 'dynamic' => true, ], ], + + 'waitlist' => [ + 'enabled' => env('WAITLIST', false), + 'expiration' => 10, + ], + + 'sentry' => [ + 'sentry_dsn' => env('SENTRY_DSN'), + ], + + 'webhooks' => [ + 'feedback_discord_webhook' => env('FEEDBACK_DISCORD_WEBHOOK'), + 'dev_webhook' => env('SERVEO_URL'), + ], + + 'bunny' => [ + 'storage_api_key' => env('BUNNY_STORAGE_API_KEY'), + 'api_key' => env('BUNNY_API_KEY'), + ], ]; diff --git a/config/coolify.php b/config/coolify.php deleted file mode 100644 index f9878fff7..000000000 --- a/config/coolify.php +++ /dev/null @@ -1,16 +0,0 @@ - 'https://coolify.io/docs/', - 'contact' => 'https://coolify.io/docs/contact', - 'feedback_discord_webhook' => env('FEEDBACK_DISCORD_WEBHOOK'), - 'self_hosted' => env('SELF_HOSTED', true), - 'waitlist' => env('WAITLIST', false), - 'license_url' => 'https://licenses.coollabs.io', - 'dev_webhook' => env('SERVEO_URL'), - 'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false), - 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), - 'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper'), - 'is_horizon_enabled' => env('HORIZON_ENABLED', true), - 'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true), -]; diff --git a/config/database.php b/config/database.php index f48a68082..6f4acbfd2 100644 --- a/config/database.php +++ b/config/database.php @@ -49,6 +49,22 @@ return [ 'search_path' => 'public', 'sslmode' => 'prefer', ], + + 'testing' => [ + 'driver' => 'pgsql', + 'url' => env('DATABASE_TEST_URL'), + 'host' => env('DB_TEST_HOST', 'postgres'), + 'port' => env('DB_TEST_PORT', '5432'), + 'database' => env('DB_TEST_DATABASE', 'coolify_test'), + 'username' => env('DB_TEST_USERNAME', 'coolify'), + 'password' => env('DB_TEST_PASSWORD', 'password'), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + ], /* diff --git a/config/redoc.php b/config/redoc.php new file mode 100644 index 000000000..439e93e7b --- /dev/null +++ b/config/redoc.php @@ -0,0 +1,28 @@ + '', + + /* + |-------------------------------------------------------------------------- + | Variables + |-------------------------------------------------------------------------- + | + | You can automatically replace variables in your OpenAPI definitions by + | adding a key value pair to the array below. This will replace any + | instances of :key with the given value. + | + */ + + 'variables' => [], + +]; diff --git a/config/sentry.php b/config/sentry.php index e8b6ab098..0efb4a0e2 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -3,11 +3,11 @@ return [ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ - 'dsn' => 'https://89552af6db48f4ca6a871ec0fc42964d@o1082494.ingest.us.sentry.io/4505347448045568', + 'dsn' => config('constants.sentry.sentry_dsn'), // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.361', + 'release' => config('constants.coolify.version'), // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/subscription.php b/config/subscription.php index 3e0182de9..a033862d2 100644 --- a/config/subscription.php +++ b/config/subscription.php @@ -6,21 +6,7 @@ return [ // Stripe 'stripe_api_key' => env('STRIPE_API_KEY', null), 'stripe_webhook_secret' => env('STRIPE_WEBHOOK_SECRET', null), - 'stripe_price_id_basic_monthly' => env('STRIPE_PRICE_ID_BASIC_MONTHLY', null), - 'stripe_price_id_basic_yearly' => env('STRIPE_PRICE_ID_BASIC_YEARLY', null), - 'stripe_price_id_pro_monthly' => env('STRIPE_PRICE_ID_PRO_MONTHLY', null), - 'stripe_price_id_pro_yearly' => env('STRIPE_PRICE_ID_PRO_YEARLY', null), - 'stripe_price_id_ultimate_monthly' => env('STRIPE_PRICE_ID_ULTIMATE_MONTHLY', null), - 'stripe_price_id_ultimate_yearly' => env('STRIPE_PRICE_ID_ULTIMATE_YEARLY', null), 'stripe_excluded_plans' => env('STRIPE_EXCLUDED_PLANS', null), - - 'stripe_price_id_basic_monthly_old' => env('STRIPE_PRICE_ID_BASIC_MONTHLY_OLD', null), - 'stripe_price_id_basic_yearly_old' => env('STRIPE_PRICE_ID_BASIC_YEARLY_OLD', null), - 'stripe_price_id_pro_monthly_old' => env('STRIPE_PRICE_ID_PRO_MONTHLY_OLD', null), - 'stripe_price_id_pro_yearly_old' => env('STRIPE_PRICE_ID_PRO_YEARLY_OLD', null), - 'stripe_price_id_ultimate_monthly_old' => env('STRIPE_PRICE_ID_ULTIMATE_MONTHLY_OLD', null), - 'stripe_price_id_ultimate_yearly_old' => env('STRIPE_PRICE_ID_ULTIMATE_YEARLY_OLD', null), - 'stripe_price_id_dynamic_monthly' => env('STRIPE_PRICE_ID_DYNAMIC_MONTHLY', null), 'stripe_price_id_dynamic_yearly' => env('STRIPE_PRICE_ID_DYNAMIC_YEARLY', null), ]; diff --git a/config/version.php b/config/version.php deleted file mode 100644 index 0e83ff40e..000000000 --- a/config/version.php +++ /dev/null @@ -1,3 +0,0 @@ - fake()->unique()->name(), + 'destination_id' => 1, + 'git_repository' => fake()->url(), + 'git_branch' => fake()->word(), + 'build_pack' => 'nixpacks', + 'ports_exposes' => '3000', + 'environment_id' => 1, + 'destination_id' => 1, + ]; + } +} diff --git a/database/factories/ServerFactory.php b/database/factories/ServerFactory.php new file mode 100644 index 000000000..29546bf56 --- /dev/null +++ b/database/factories/ServerFactory.php @@ -0,0 +1,17 @@ + fake()->unique()->name(), + 'ip' => fake()->unique()->ipv4(), + 'private_key_id' => 1, + ]; + } +} diff --git a/database/migrations/2024_06_25_184323_update_db.php b/database/migrations/2024_06_25_184323_update_db.php index 8f9405b86..d9cddb15f 100644 --- a/database/migrations/2024_06_25_184323_update_db.php +++ b/database/migrations/2024_06_25_184323_update_db.php @@ -5,6 +5,7 @@ use App\Models\Server; use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Schema; use Visus\Cuid2\Cuid2; @@ -52,7 +53,7 @@ return new class extends Migration DB::table('server_settings')->update(['metrics_history_days' => 7]); } catch (\Exception $e) { - loggy($e); + Log::error('Error updating db: '.$e->getMessage()); } } diff --git a/database/migrations/2024_11_11_125335_add_custom_nginx_configuration_to_static.php b/database/migrations/2024_11_11_125335_add_custom_nginx_configuration_to_static.php new file mode 100644 index 000000000..1c6e2daa0 --- /dev/null +++ b/database/migrations/2024_11_11_125335_add_custom_nginx_configuration_to_static.php @@ -0,0 +1,28 @@ +longText('custom_nginx_configuration')->nullable()->after('static_image'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('custom_nginx_configuration'); + }); + } +}; diff --git a/database/migrations/2024_11_11_125366_add_index_to_activity_log.php b/database/migrations/2024_11_11_125366_add_index_to_activity_log.php new file mode 100644 index 000000000..0c281ff40 --- /dev/null +++ b/database/migrations/2024_11_11_125366_add_index_to_activity_log.php @@ -0,0 +1,28 @@ +getMessage()); + } + } + + public function down() + { + try { + DB::statement('DROP INDEX IF EXISTS idx_activity_type_uuid'); + DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE json USING properties::json'); + } catch (\Exception $e) { + Log::error('Error dropping index from activity_log: '.$e->getMessage()); + } + } +} diff --git a/database/migrations/2024_11_12_213200_add_slack_notifications_to_teams_table.php b/database/migrations/2024_11_12_213200_add_slack_notifications_to_teams_table.php new file mode 100644 index 000000000..a6457269a --- /dev/null +++ b/database/migrations/2024_11_12_213200_add_slack_notifications_to_teams_table.php @@ -0,0 +1,38 @@ +boolean('slack_enabled')->default(false); + $table->string('slack_webhook_url')->nullable(); + $table->boolean('slack_notifications_test')->default(true); + $table->boolean('slack_notifications_deployments')->default(true); + $table->boolean('slack_notifications_status_changes')->default(true); + $table->boolean('slack_notifications_database_backups')->default(true); + $table->boolean('slack_notifications_scheduled_tasks')->default(true); + $table->boolean('slack_notifications_server_disk_usage')->default(true); + }); + } + + public function down(): void + { + Schema::table('teams', function (Blueprint $table) { + $table->dropColumn([ + 'slack_enabled', + 'slack_webhook_url', + 'slack_notifications_test', + 'slack_notifications_deployments', + 'slack_notifications_status_changes', + 'slack_notifications_database_backups', + 'slack_notifications_scheduled_tasks', + 'slack_notifications_server_disk_usage', + ]); + }); + } +}; diff --git a/database/migrations/2024_12_05_091823_add_disable_build_cache_advanced_option.php b/database/migrations/2024_12_05_091823_add_disable_build_cache_advanced_option.php new file mode 100644 index 000000000..751342302 --- /dev/null +++ b/database/migrations/2024_12_05_091823_add_disable_build_cache_advanced_option.php @@ -0,0 +1,22 @@ +boolean('disable_build_cache')->default(false); + }); + } + + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('disable_build_cache'); + }); + } +}; diff --git a/database/seeders/GithubAppSeeder.php b/database/seeders/GithubAppSeeder.php index 2ece7a05b..3cfb82e64 100644 --- a/database/seeders/GithubAppSeeder.php +++ b/database/seeders/GithubAppSeeder.php @@ -23,6 +23,7 @@ class GithubAppSeeder extends Seeder GithubApp::create([ 'name' => 'coolify-laravel-development-public', 'uuid' => '69420', + 'organization' => 'coollabsio', 'api_url' => 'https://api.github.com', 'html_url' => 'https://github.com', 'is_public' => false, diff --git a/database/seeders/PopulateSshKeysDirectorySeeder.php b/database/seeders/PopulateSshKeysDirectorySeeder.php index d528179c0..87984769c 100644 --- a/database/seeders/PopulateSshKeysDirectorySeeder.php +++ b/database/seeders/PopulateSshKeysDirectorySeeder.php @@ -24,9 +24,8 @@ class PopulateSshKeysDirectorySeeder extends Seeder }); if (isDev()) { - $user = env('PUID').':'.env('PGID'); - Process::run("chown -R $user ".storage_path('app/ssh/keys')); - Process::run("chown -R $user ".storage_path('app/ssh/mux')); + Process::run('chown -R 9999:9999 '.storage_path('app/ssh/keys')); + Process::run('chown -R 9999:9999 '.storage_path('app/ssh/mux')); } else { Process::run('chown -R 9999:root '.storage_path('app/ssh/keys')); Process::run('chown -R 9999:root '.storage_path('app/ssh/mux')); diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php index 3e820a162..7acdef548 100644 --- a/database/seeders/ProductionSeeder.php +++ b/database/seeders/ProductionSeeder.php @@ -22,9 +22,9 @@ class ProductionSeeder extends Seeder public function run(): void { if (isCloud()) { - echo "Running in cloud mode.\n"; + echo "[x]: Running in cloud mode.\n"; } else { - echo "Running in self-hosted mode.\n"; + echo "[x]: Running in self-hosted mode.\n"; } // Fix for 4.0.0-beta.37 @@ -100,7 +100,7 @@ class ProductionSeeder extends Seeder } } - if (! isCloud() && config('coolify.is_windows_docker_desktop') == false) { + if (! isCloud() && config('constants.coolify.is_windows_docker_desktop') == false) { $coolify_key_name = '@host.docker.internal'; $ssh_keys_directory = Storage::disk('ssh-keys')->files(); $coolify_key = collect($ssh_keys_directory)->firstWhere(fn ($item) => str($item)->contains($coolify_key_name)); @@ -127,7 +127,7 @@ class ProductionSeeder extends Seeder } } } - if (config('coolify.is_windows_docker_desktop')) { + if (config('constants.coolify.is_windows_docker_desktop')) { PrivateKey::updateOrCreate( [ 'id' => 0, diff --git a/database/seeders/SentinelSeeder.php b/database/seeders/SentinelSeeder.php index 117ba6782..3cf913933 100644 --- a/database/seeders/SentinelSeeder.php +++ b/database/seeders/SentinelSeeder.php @@ -4,6 +4,7 @@ namespace Database\Seeders; use App\Models\Server; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Log; class SentinelSeeder extends Seeder { @@ -13,17 +14,17 @@ class SentinelSeeder extends Seeder foreach ($servers as $server) { try { if (str($server->settings->sentinel_token)->isEmpty()) { - $server->settings->generateSentinelToken(); + $server->settings->generateSentinelToken(ignoreEvent: true); } if (str($server->settings->sentinel_custom_url)->isEmpty()) { - $url = $server->settings->generateSentinelUrl(); + $url = $server->settings->generateSentinelUrl(ignoreEvent: true); if (str($url)->isEmpty()) { $server->settings->is_sentinel_enabled = false; $server->settings->save(); } } } catch (\Throwable $e) { - loggy("Error: {$e->getMessage()}\n"); + Log::error('Error seeding sentinel: '.$e->getMessage()); } } }); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f1da567fc..05b9f9cfb 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -60,7 +60,7 @@ services: SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}" entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"] vite: - image: node:20 + image: node:20-alpine pull_policy: always working_dir: /var/www/html environment: @@ -74,8 +74,9 @@ services: networks: - coolify testing-host: - image: "ghcr.io/coollabsio/coolify-testing-host:latest" - pull_policy: always + build: + context: . + dockerfile: ./docker/testing-host/Dockerfile init: true container_name: coolify-testing-host volumes: @@ -88,7 +89,7 @@ services: networks: - coolify mailpit: - image: "axllent/mailpit:latest" + image: axllent/mailpit:latest pull_policy: always container_name: coolify-mail ports: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b15a109c3..d86b2336b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -29,6 +29,7 @@ services: - REDIS_HOST - REDIS_PASSWORD - HORIZON_BALANCE + - HORIZON_MIN_PROCESSES - HORIZON_MAX_PROCESSES - HORIZON_BALANCE_MAX_SHIFT - HORIZON_BALANCE_COOLDOWN @@ -50,29 +51,8 @@ services: - TERMINAL_HOST - TERMINAL_PORT - AUTOUPDATE - - SELF_HOSTED - SSH_MUX_ENABLED - SSH_MUX_PERSIST_TIME - - FEEDBACK_DISCORD_WEBHOOK - - WAITLIST - - SUBSCRIPTION_PROVIDER - - STRIPE_API_KEY - - STRIPE_WEBHOOK_SECRET - - STRIPE_PRICE_ID_BASIC_MONTHLY - - STRIPE_PRICE_ID_BASIC_YEARLY - - STRIPE_PRICE_ID_PRO_MONTHLY - - STRIPE_PRICE_ID_PRO_YEARLY - - STRIPE_PRICE_ID_ULTIMATE_MONTHLY - - STRIPE_PRICE_ID_ULTIMATE_YEARLY - - STRIPE_PRICE_ID_DYNAMIC_MONTHLY - - STRIPE_PRICE_ID_DYNAMIC_YEARLY - - STRIPE_PRICE_ID_BASIC_MONTHLY_OLD - - STRIPE_PRICE_ID_BASIC_YEARLY_OLD - - STRIPE_PRICE_ID_PRO_MONTHLY_OLD - - STRIPE_PRICE_ID_PRO_YEARLY_OLD - - STRIPE_PRICE_ID_ULTIMATE_MONTHLY_OLD - - STRIPE_PRICE_ID_ULTIMATE_YEARLY_OLD - - STRIPE_EXCLUDED_PLANS ports: - "${APP_PORT:-8000}:80" expose: @@ -113,7 +93,7 @@ services: retries: 10 timeout: 2s soketi: - image: 'ghcr.io/coollabsio/coolify-realtime:1.0.3' + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.4' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml index ef2de82e9..f2b03d209 100644 --- a/docker-compose.windows.yml +++ b/docker-compose.windows.yml @@ -86,7 +86,7 @@ services: retries: 10 timeout: 2s redis: - image: redis:alpine + image: redis:7-alpine pull_policy: always container_name: coolify-redis restart: always @@ -103,7 +103,7 @@ services: retries: 10 timeout: 2s soketi: - image: 'ghcr.io/coollabsio/coolify-realtime:1.0.0' + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.4' pull_policy: always container_name: coolify-realtime restart: always diff --git a/docker-compose.yml b/docker-compose.yml index 68d0f0744..0fd3dda07 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: restart: always working_dir: /var/www/html extra_hosts: - - 'host.docker.internal:host-gateway' + - host.docker.internal:host-gateway networks: - coolify depends_on: @@ -18,7 +18,7 @@ services: networks: - coolify redis: - image: redis:alpine + image: redis:7-alpine container_name: coolify-redis restart: always networks: @@ -26,7 +26,7 @@ services: soketi: container_name: coolify-realtime extra_hosts: - - 'host.docker.internal:host-gateway' + - host.docker.internal:host-gateway restart: always networks: - coolify diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index 7aa9d8722..741fff764 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -1,16 +1,30 @@ -FROM alpine:3.17 +# Versions -ARG TARGETPLATFORM +# https://hub.docker.com/_/alpine +ARG BASE_IMAGE=alpine:3.20 # https://download.docker.com/linux/static/stable/ -ARG DOCKER_VERSION=26.1.3 +ARG DOCKER_VERSION=27.3.1 # https://github.com/docker/compose/releases -ARG DOCKER_COMPOSE_VERSION=2.27.1 +ARG DOCKER_COMPOSE_VERSION=2.30.3 # https://github.com/docker/buildx/releases -ARG DOCKER_BUILDX_VERSION=0.14.1 +ARG DOCKER_BUILDX_VERSION=0.18.0 # https://github.com/buildpacks/pack/releases ARG PACK_VERSION=0.35.1 # https://github.com/railwayapp/nixpacks/releases -ARG NIXPACKS_VERSION=1.28.0 +ARG NIXPACKS_VERSION=1.29.0 +# https://hub.docker.com/r/minio/mc/tags +ARG MINIO_VERSION=RELEASE.2024-03-07T00-31-49Z + +FROM minio/mc:${MINIO_VERSION} AS minio-client + +FROM ${BASE_IMAGE} AS base + +ARG TARGETPLATFORM +ARG DOCKER_VERSION +ARG DOCKER_COMPOSE_VERSION +ARG DOCKER_BUILDX_VERSION +ARG PACK_VERSION +ARG NIXPACKS_VERSION USER root WORKDIR /artifacts @@ -34,9 +48,8 @@ RUN if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \ chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack /root/.docker/cli-plugins/docker-buildx \ ;fi -COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc +COPY --from=minio-client /usr/bin/mc /usr/bin/mc RUN chmod +x /usr/bin/mc ENTRYPOINT ["/sbin/tini", "--"] CMD ["tail", "-f", "/dev/null"] - diff --git a/docker/coolify-realtime/Dockerfile b/docker/coolify-realtime/Dockerfile index f0d6db906..b9f3c1e62 100644 --- a/docker/coolify-realtime/Dockerfile +++ b/docker/coolify-realtime/Dockerfile @@ -1,5 +1,6 @@ FROM quay.io/soketi/soketi:1.6-16-alpine + ARG TARGETPLATFORM # https://github.com/cloudflare/cloudflared/releases ARG CLOUDFLARED_VERSION=2024.4.1 @@ -22,6 +23,4 @@ RUN /bin/sh -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \ curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ ;fi" - - ENTRYPOINT ["/bin/sh", "/soketi-entrypoint.sh"] diff --git a/docker/coolify-realtime/package-lock.json b/docker/coolify-realtime/package-lock.json new file mode 100644 index 000000000..9316442ae --- /dev/null +++ b/docker/coolify-realtime/package-lock.json @@ -0,0 +1,190 @@ +{ + "name": "coolify-realtime", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@xterm/addon-fit": "0.10.0", + "@xterm/xterm": "5.5.0", + "axios": "1.7.7", + "cookie": "1.0.1", + "dotenv": "16.4.5", + "node-pty": "1.0.0", + "ws": "8.18.0" + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cookie": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.1.tgz", + "integrity": "sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "license": "MIT" + }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "nan": "^2.17.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/docker/coolify-realtime/package.json b/docker/coolify-realtime/package.json index 146e6b90a..8b2076f8d 100644 --- a/docker/coolify-realtime/package.json +++ b/docker/coolify-realtime/package.json @@ -2,12 +2,12 @@ "private": true, "type": "module", "dependencies": { - "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.5.0", - "cookie": "^0.7.0", - "axios": "1.7.5", - "dotenv": "^16.4.5", - "node-pty": "^1.0.0", - "ws": "^8.17.0" + "@xterm/addon-fit": "0.10.0", + "@xterm/xterm": "5.5.0", + "cookie": "1.0.1", + "axios": "1.7.7", + "dotenv": "16.4.5", + "node-pty": "1.0.0", + "ws": "8.18.0" } } diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index d2381f764..e90521759 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -1,10 +1,21 @@ -FROM serversideup/php:8.2-fpm-nginx-v2.2.1 +# Versions +# https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-nginx-alpine +ARG SERVERSIDEUP_PHP_VERSION=8.2-fpm-nginx-v2.2.1 +# https://github.com/minio/mc/releases +ARG MINIO_VERSION=RELEASE.2024-11-05T11-29-45Z +# https://github.com/cloudflare/cloudflared/releases +ARG CLOUDFLARED_VERSION=2024.11.0 +# https://www.postgresql.org/support/versioning/ - Can not updated automatically so keep it at 15 +ARG POSTGRES_VERSION=15 + +FROM minio/mc:${MINIO_VERSION} AS minio-client + +FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION} ARG TARGETPLATFORM -# https://github.com/cloudflare/cloudflared/releases -ARG CLOUDFLARED_VERSION=2024.4.1 - -ARG POSTGRES_VERSION=15 +ARG CLOUDFLARED_VERSION +ARG MINIO_VERSION +ARG POSTGRES_VERSION # Use build arguments for caching ARG BUILDTIME_DEPS="dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl" @@ -41,7 +52,7 @@ RUN --mount=type=cache,target=/root/.cache \ curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ ;fi" -COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc +COPY --from=minio-client /usr/bin/mc /usr/bin/mc RUN chmod +x /usr/bin/mc RUN { \ diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index 22a5b143a..36be6cf06 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -1,22 +1,34 @@ -FROM serversideup/php:8.2-fpm-nginx-v2.2.1 AS base +# Versions +# https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-nginx-alpine +ARG SERVERSIDEUP_PHP_VERSION=8.2-fpm-nginx-v2.2.1 +# https://github.com/minio/mc/releases +ARG MINIO_VERSION=RELEASE.2024-11-05T11-29-45Z +# https://github.com/cloudflare/cloudflared/releases +ARG CLOUDFLARED_VERSION=2024.11.0 +# https://www.postgresql.org/support/versioning/ - Can not updated automatically so keep it at 15 +ARG POSTGRES_VERSION=15 + + +FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION} AS base WORKDIR /var/www/html COPY composer.json composer.lock ./ RUN composer install --no-dev --no-interaction --no-plugins --no-scripts --prefer-dist -FROM node:20 as static-assets +FROM node:20 AS static-assets WORKDIR /app COPY . . COPY --from=base --chown=9999:9999 /var/www/html . RUN npm install RUN npm run build -FROM serversideup/php:8.2-fpm-nginx-v2.2.1 +FROM minio/mc:${MINIO_VERSION} AS minio-client + +FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION} ARG TARGETPLATFORM -# https://github.com/cloudflare/cloudflared/releases -ARG CLOUDFLARED_VERSION=2024.4.1 -ARG POSTGRES_VERSION=15 +ARG CLOUDFLARED_VERSION +ARG POSTGRES_VERSION ARG CI=true WORKDIR /var/www/html @@ -29,7 +41,7 @@ RUN curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmo RUN echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list RUN apt-get update -RUN apt-get install postgresql-client-$POSTGRES_VERSION -y +RUN apt-get install postgresql-client-${POSTGRES_VERSION} -y # Coolify requirements RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof vim @@ -45,9 +57,6 @@ RUN composer dump-autoload COPY --from=static-assets --chown=9999:9999 /app/public/build ./public/build COPY --chmod=755 docker/prod/etc/s6-overlay/ /etc/s6-overlay/ -RUN php artisan route:cache -RUN php artisan view:cache - RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc RUN echo "alias a='php artisan'" >>/etc/bash.bashrc RUN echo "alias logs='tail -f storage/logs/laravel.log'" >>/etc/bash.bashrc @@ -69,5 +78,5 @@ RUN { \ echo 'post_max_size=256M'; \ } > /etc/php/current_version/cli/conf.d/upload-limits.ini -COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc +COPY --from=minio-client /usr/bin/mc /usr/bin/mc RUN chmod +x /usr/bin/mc diff --git a/docker/testing-host/Dockerfile b/docker/testing-host/Dockerfile index deb09eeba..d98fcc821 100644 --- a/docker/testing-host/Dockerfile +++ b/docker/testing-host/Dockerfile @@ -1,16 +1,22 @@ +# Versions +# https://download.docker.com/linux/static/stable/ +ARG DOCKER_VERSION=27.3.1 +# https://github.com/docker/compose/releases +ARG DOCKER_COMPOSE_VERSION=2.30.3 +# https://github.com/docker/buildx/releases +ARG DOCKER_BUILDX_VERSION=0.18.0 + + FROM debian:12-slim ARG TARGETPLATFORM -# https://download.docker.com/linux/static/stable/ -ARG DOCKER_VERSION=26.1.2 -# https://github.com/docker/compose/releases -ARG DOCKER_COMPOSE_VERSION=2.27.0 -# https://github.com/docker/buildx/releases -ARG DOCKER_BUILDX_VERSION=0.14.0 +ARG DOCKER_VERSION +ARG DOCKER_COMPOSE_VERSION +ARG DOCKER_BUILDX_VERSION USER root WORKDIR /root -ENV PATH "$PATH:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin" +ENV PATH="/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin:$PATH" RUN apt update && apt -y install openssh-client openssh-server curl wget git jq jc RUN mkdir -p ~/.docker/cli-plugins diff --git a/examples/traefik-dynamic-catch-all.yaml b/examples/traefik-dynamic-catch-all.yaml deleted file mode 100644 index 54f7b1fb9..000000000 --- a/examples/traefik-dynamic-catch-all.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# This is an example dynamic configuration. -http: - routers: - catchall: - entryPoints: - - http - - https - service: noop - rule: HostRegexp(`{catchall:.*}`) - priority: 1 - middlewares: - - redirect-regexp - services: - noop: - loadBalancer: - servers: - - url: '' - middlewares: - redirect-regexp: - redirectRegex: - regex: '(.*)' - replacement: 'https://coolify.io' - permanent: false \ No newline at end of file diff --git a/examples/traefik-dynamic-coolify.yaml b/examples/traefik-dynamic-coolify.yaml deleted file mode 100644 index 0c5f7e311..000000000 --- a/examples/traefik-dynamic-coolify.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# This is an example dynamic configuration. -http: - routers: - coolify-http: - entryPoints: - - http - service: coolify - rule: Host(`coolify.io`) - services: - coolify: - loadBalancer: - servers: - - - url: 'http://coolify:80' \ No newline at end of file diff --git a/openapi.json b/openapi.json new file mode 100644 index 000000000..5d35331ec --- /dev/null +++ b/openapi.json @@ -0,0 +1,8004 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Coolify", + "version": "0.1" + }, + "servers": [ + { + "url": "https:\/\/app.coolify.io\/api\/v1", + "description": "Coolify Cloud API. Change the host to your own instance if you are self-hosting." + } + ], + "paths": { + "\/applications": { + "get": { + "tags": [ + "Applications" + ], + "summary": "List", + "description": "List all applications.", + "operationId": "list-applications", + "responses": { + "200": { + "description": "Get all applications.", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/Application" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/applications\/public": { + "post": { + "tags": [ + "Applications" + ], + "summary": "Create (Public)", + "description": "Create new application based on a public git repository.", + "operationId": "create-public-application", + "requestBody": { + "description": "Application object that needs to be created.", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "project_uuid", + "server_uuid", + "environment_name", + "git_repository", + "git_branch", + "build_pack", + "ports_exposes" + ], + "properties": { + "project_uuid": { + "type": "string", + "description": "The project UUID." + }, + "server_uuid": { + "type": "string", + "description": "The server UUID." + }, + "environment_name": { + "type": "string", + "description": "The environment name." + }, + "git_repository": { + "type": "string", + "description": "The git repository URL." + }, + "git_branch": { + "type": "string", + "description": "The git branch." + }, + "build_pack": { + "type": "string", + "enum": [ + "nixpacks", + "static", + "dockerfile", + "dockercompose" + ], + "description": "The build pack type." + }, + "ports_exposes": { + "type": "string", + "description": "The ports to expose." + }, + "destination_uuid": { + "type": "string", + "description": "The destination UUID." + }, + "name": { + "type": "string", + "description": "The application name." + }, + "description": { + "type": "string", + "description": "The application description." + }, + "domains": { + "type": "string", + "description": "The application domains." + }, + "git_commit_sha": { + "type": "string", + "description": "The git commit SHA." + }, + "docker_registry_image_name": { + "type": "string", + "description": "The docker registry image name." + }, + "docker_registry_image_tag": { + "type": "string", + "description": "The docker registry image tag." + }, + "is_static": { + "type": "boolean", + "description": "The flag to indicate if the application is static." + }, + "static_image": { + "type": "string", + "enum": [ + "nginx:alpine" + ], + "description": "The static image." + }, + "install_command": { + "type": "string", + "description": "The install command." + }, + "build_command": { + "type": "string", + "description": "The build command." + }, + "start_command": { + "type": "string", + "description": "The start command." + }, + "ports_mappings": { + "type": "string", + "description": "The ports mappings." + }, + "base_directory": { + "type": "string", + "description": "The base directory for all commands." + }, + "publish_directory": { + "type": "string", + "description": "The publish directory." + }, + "health_check_enabled": { + "type": "boolean", + "description": "Health check enabled." + }, + "health_check_path": { + "type": "string", + "description": "Health check path." + }, + "health_check_port": { + "type": "string", + "nullable": true, + "description": "Health check port." + }, + "health_check_host": { + "type": "string", + "nullable": true, + "description": "Health check host." + }, + "health_check_method": { + "type": "string", + "description": "Health check method." + }, + "health_check_return_code": { + "type": "integer", + "description": "Health check return code." + }, + "health_check_scheme": { + "type": "string", + "description": "Health check scheme." + }, + "health_check_response_text": { + "type": "string", + "nullable": true, + "description": "Health check response text." + }, + "health_check_interval": { + "type": "integer", + "description": "Health check interval in seconds." + }, + "health_check_timeout": { + "type": "integer", + "description": "Health check timeout in seconds." + }, + "health_check_retries": { + "type": "integer", + "description": "Health check retries count." + }, + "health_check_start_period": { + "type": "integer", + "description": "Health check start period in seconds." + }, + "limits_memory": { + "type": "string", + "description": "Memory limit." + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit." + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness." + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation." + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit." + }, + "limits_cpuset": { + "type": "string", + "nullable": true, + "description": "CPU set." + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares." + }, + "custom_labels": { + "type": "string", + "description": "Custom labels." + }, + "custom_docker_run_options": { + "type": "string", + "description": "Custom docker run options." + }, + "post_deployment_command": { + "type": "string", + "description": "Post deployment command." + }, + "post_deployment_command_container": { + "type": "string", + "description": "Post deployment command container." + }, + "pre_deployment_command": { + "type": "string", + "description": "Pre deployment command." + }, + "pre_deployment_command_container": { + "type": "string", + "description": "Pre deployment command container." + }, + "manual_webhook_secret_github": { + "type": "string", + "description": "Manual webhook secret for Github." + }, + "manual_webhook_secret_gitlab": { + "type": "string", + "description": "Manual webhook secret for Gitlab." + }, + "manual_webhook_secret_bitbucket": { + "type": "string", + "description": "Manual webhook secret for Bitbucket." + }, + "manual_webhook_secret_gitea": { + "type": "string", + "description": "Manual webhook secret for Gitea." + }, + "redirect": { + "type": "string", + "nullable": true, + "description": "How to set redirect with Traefik \/ Caddy. www<->non-www.", + "enum": [ + "www", + "non-www", + "both" + ] + }, + "instant_deploy": { + "type": "boolean", + "description": "The flag to indicate if the application should be deployed instantly." + }, + "dockerfile": { + "type": "string", + "description": "The Dockerfile content." + }, + "docker_compose_location": { + "type": "string", + "description": "The Docker Compose location." + }, + "docker_compose_raw": { + "type": "string", + "description": "The Docker Compose raw content." + }, + "docker_compose_custom_start_command": { + "type": "string", + "description": "The Docker Compose custom start command." + }, + "docker_compose_custom_build_command": { + "type": "string", + "description": "The Docker Compose custom build command." + }, + "docker_compose_domains": { + "type": "array", + "description": "The Docker Compose domains." + }, + "watch_paths": { + "type": "string", + "description": "The watch paths." + }, + "use_build_server": { + "type": "boolean", + "nullable": true, + "description": "Use build server." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Application created successfully." + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/applications\/private-github-app": { + "post": { + "tags": [ + "Applications" + ], + "summary": "Create (Private - GH App)", + "description": "Create new application based on a private repository through a Github App.", + "operationId": "create-private-github-app-application", + "requestBody": { + "description": "Application object that needs to be created.", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "project_uuid", + "server_uuid", + "environment_name", + "github_app_uuid", + "git_repository", + "git_branch", + "build_pack", + "ports_exposes" + ], + "properties": { + "project_uuid": { + "type": "string", + "description": "The project UUID." + }, + "server_uuid": { + "type": "string", + "description": "The server UUID." + }, + "environment_name": { + "type": "string", + "description": "The environment name." + }, + "github_app_uuid": { + "type": "string", + "description": "The Github App UUID." + }, + "git_repository": { + "type": "string", + "description": "The git repository URL." + }, + "git_branch": { + "type": "string", + "description": "The git branch." + }, + "ports_exposes": { + "type": "string", + "description": "The ports to expose." + }, + "destination_uuid": { + "type": "string", + "description": "The destination UUID." + }, + "build_pack": { + "type": "string", + "enum": [ + "nixpacks", + "static", + "dockerfile", + "dockercompose" + ], + "description": "The build pack type." + }, + "name": { + "type": "string", + "description": "The application name." + }, + "description": { + "type": "string", + "description": "The application description." + }, + "domains": { + "type": "string", + "description": "The application domains." + }, + "git_commit_sha": { + "type": "string", + "description": "The git commit SHA." + }, + "docker_registry_image_name": { + "type": "string", + "description": "The docker registry image name." + }, + "docker_registry_image_tag": { + "type": "string", + "description": "The docker registry image tag." + }, + "is_static": { + "type": "boolean", + "description": "The flag to indicate if the application is static." + }, + "static_image": { + "type": "string", + "enum": [ + "nginx:alpine" + ], + "description": "The static image." + }, + "install_command": { + "type": "string", + "description": "The install command." + }, + "build_command": { + "type": "string", + "description": "The build command." + }, + "start_command": { + "type": "string", + "description": "The start command." + }, + "ports_mappings": { + "type": "string", + "description": "The ports mappings." + }, + "base_directory": { + "type": "string", + "description": "The base directory for all commands." + }, + "publish_directory": { + "type": "string", + "description": "The publish directory." + }, + "health_check_enabled": { + "type": "boolean", + "description": "Health check enabled." + }, + "health_check_path": { + "type": "string", + "description": "Health check path." + }, + "health_check_port": { + "type": "string", + "nullable": true, + "description": "Health check port." + }, + "health_check_host": { + "type": "string", + "nullable": true, + "description": "Health check host." + }, + "health_check_method": { + "type": "string", + "description": "Health check method." + }, + "health_check_return_code": { + "type": "integer", + "description": "Health check return code." + }, + "health_check_scheme": { + "type": "string", + "description": "Health check scheme." + }, + "health_check_response_text": { + "type": "string", + "nullable": true, + "description": "Health check response text." + }, + "health_check_interval": { + "type": "integer", + "description": "Health check interval in seconds." + }, + "health_check_timeout": { + "type": "integer", + "description": "Health check timeout in seconds." + }, + "health_check_retries": { + "type": "integer", + "description": "Health check retries count." + }, + "health_check_start_period": { + "type": "integer", + "description": "Health check start period in seconds." + }, + "limits_memory": { + "type": "string", + "description": "Memory limit." + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit." + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness." + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation." + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit." + }, + "limits_cpuset": { + "type": "string", + "nullable": true, + "description": "CPU set." + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares." + }, + "custom_labels": { + "type": "string", + "description": "Custom labels." + }, + "custom_docker_run_options": { + "type": "string", + "description": "Custom docker run options." + }, + "post_deployment_command": { + "type": "string", + "description": "Post deployment command." + }, + "post_deployment_command_container": { + "type": "string", + "description": "Post deployment command container." + }, + "pre_deployment_command": { + "type": "string", + "description": "Pre deployment command." + }, + "pre_deployment_command_container": { + "type": "string", + "description": "Pre deployment command container." + }, + "manual_webhook_secret_github": { + "type": "string", + "description": "Manual webhook secret for Github." + }, + "manual_webhook_secret_gitlab": { + "type": "string", + "description": "Manual webhook secret for Gitlab." + }, + "manual_webhook_secret_bitbucket": { + "type": "string", + "description": "Manual webhook secret for Bitbucket." + }, + "manual_webhook_secret_gitea": { + "type": "string", + "description": "Manual webhook secret for Gitea." + }, + "redirect": { + "type": "string", + "nullable": true, + "description": "How to set redirect with Traefik \/ Caddy. www<->non-www.", + "enum": [ + "www", + "non-www", + "both" + ] + }, + "instant_deploy": { + "type": "boolean", + "description": "The flag to indicate if the application should be deployed instantly." + }, + "dockerfile": { + "type": "string", + "description": "The Dockerfile content." + }, + "docker_compose_location": { + "type": "string", + "description": "The Docker Compose location." + }, + "docker_compose_raw": { + "type": "string", + "description": "The Docker Compose raw content." + }, + "docker_compose_custom_start_command": { + "type": "string", + "description": "The Docker Compose custom start command." + }, + "docker_compose_custom_build_command": { + "type": "string", + "description": "The Docker Compose custom build command." + }, + "docker_compose_domains": { + "type": "array", + "description": "The Docker Compose domains." + }, + "watch_paths": { + "type": "string", + "description": "The watch paths." + }, + "use_build_server": { + "type": "boolean", + "nullable": true, + "description": "Use build server." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Application created successfully." + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/applications\/private-deploy-key": { + "post": { + "tags": [ + "Applications" + ], + "summary": "Create (Private - Deploy Key)", + "description": "Create new application based on a private repository through a Deploy Key.", + "operationId": "create-private-deploy-key-application", + "requestBody": { + "description": "Application object that needs to be created.", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "project_uuid", + "server_uuid", + "environment_name", + "private_key_uuid", + "git_repository", + "git_branch", + "build_pack", + "ports_exposes" + ], + "properties": { + "project_uuid": { + "type": "string", + "description": "The project UUID." + }, + "server_uuid": { + "type": "string", + "description": "The server UUID." + }, + "environment_name": { + "type": "string", + "description": "The environment name." + }, + "private_key_uuid": { + "type": "string", + "description": "The private key UUID." + }, + "git_repository": { + "type": "string", + "description": "The git repository URL." + }, + "git_branch": { + "type": "string", + "description": "The git branch." + }, + "ports_exposes": { + "type": "string", + "description": "The ports to expose." + }, + "destination_uuid": { + "type": "string", + "description": "The destination UUID." + }, + "build_pack": { + "type": "string", + "enum": [ + "nixpacks", + "static", + "dockerfile", + "dockercompose" + ], + "description": "The build pack type." + }, + "name": { + "type": "string", + "description": "The application name." + }, + "description": { + "type": "string", + "description": "The application description." + }, + "domains": { + "type": "string", + "description": "The application domains." + }, + "git_commit_sha": { + "type": "string", + "description": "The git commit SHA." + }, + "docker_registry_image_name": { + "type": "string", + "description": "The docker registry image name." + }, + "docker_registry_image_tag": { + "type": "string", + "description": "The docker registry image tag." + }, + "is_static": { + "type": "boolean", + "description": "The flag to indicate if the application is static." + }, + "static_image": { + "type": "string", + "enum": [ + "nginx:alpine" + ], + "description": "The static image." + }, + "install_command": { + "type": "string", + "description": "The install command." + }, + "build_command": { + "type": "string", + "description": "The build command." + }, + "start_command": { + "type": "string", + "description": "The start command." + }, + "ports_mappings": { + "type": "string", + "description": "The ports mappings." + }, + "base_directory": { + "type": "string", + "description": "The base directory for all commands." + }, + "publish_directory": { + "type": "string", + "description": "The publish directory." + }, + "health_check_enabled": { + "type": "boolean", + "description": "Health check enabled." + }, + "health_check_path": { + "type": "string", + "description": "Health check path." + }, + "health_check_port": { + "type": "string", + "nullable": true, + "description": "Health check port." + }, + "health_check_host": { + "type": "string", + "nullable": true, + "description": "Health check host." + }, + "health_check_method": { + "type": "string", + "description": "Health check method." + }, + "health_check_return_code": { + "type": "integer", + "description": "Health check return code." + }, + "health_check_scheme": { + "type": "string", + "description": "Health check scheme." + }, + "health_check_response_text": { + "type": "string", + "nullable": true, + "description": "Health check response text." + }, + "health_check_interval": { + "type": "integer", + "description": "Health check interval in seconds." + }, + "health_check_timeout": { + "type": "integer", + "description": "Health check timeout in seconds." + }, + "health_check_retries": { + "type": "integer", + "description": "Health check retries count." + }, + "health_check_start_period": { + "type": "integer", + "description": "Health check start period in seconds." + }, + "limits_memory": { + "type": "string", + "description": "Memory limit." + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit." + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness." + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation." + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit." + }, + "limits_cpuset": { + "type": "string", + "nullable": true, + "description": "CPU set." + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares." + }, + "custom_labels": { + "type": "string", + "description": "Custom labels." + }, + "custom_docker_run_options": { + "type": "string", + "description": "Custom docker run options." + }, + "post_deployment_command": { + "type": "string", + "description": "Post deployment command." + }, + "post_deployment_command_container": { + "type": "string", + "description": "Post deployment command container." + }, + "pre_deployment_command": { + "type": "string", + "description": "Pre deployment command." + }, + "pre_deployment_command_container": { + "type": "string", + "description": "Pre deployment command container." + }, + "manual_webhook_secret_github": { + "type": "string", + "description": "Manual webhook secret for Github." + }, + "manual_webhook_secret_gitlab": { + "type": "string", + "description": "Manual webhook secret for Gitlab." + }, + "manual_webhook_secret_bitbucket": { + "type": "string", + "description": "Manual webhook secret for Bitbucket." + }, + "manual_webhook_secret_gitea": { + "type": "string", + "description": "Manual webhook secret for Gitea." + }, + "redirect": { + "type": "string", + "nullable": true, + "description": "How to set redirect with Traefik \/ Caddy. www<->non-www.", + "enum": [ + "www", + "non-www", + "both" + ] + }, + "instant_deploy": { + "type": "boolean", + "description": "The flag to indicate if the application should be deployed instantly." + }, + "dockerfile": { + "type": "string", + "description": "The Dockerfile content." + }, + "docker_compose_location": { + "type": "string", + "description": "The Docker Compose location." + }, + "docker_compose_raw": { + "type": "string", + "description": "The Docker Compose raw content." + }, + "docker_compose_custom_start_command": { + "type": "string", + "description": "The Docker Compose custom start command." + }, + "docker_compose_custom_build_command": { + "type": "string", + "description": "The Docker Compose custom build command." + }, + "docker_compose_domains": { + "type": "array", + "description": "The Docker Compose domains." + }, + "watch_paths": { + "type": "string", + "description": "The watch paths." + }, + "use_build_server": { + "type": "boolean", + "nullable": true, + "description": "Use build server." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Application created successfully." + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/applications\/dockerfile": { + "post": { + "tags": [ + "Applications" + ], + "summary": "Create (Dockerfile)", + "description": "Create new application based on a simple Dockerfile.", + "operationId": "create-dockerfile-application", + "requestBody": { + "description": "Application object that needs to be created.", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "project_uuid", + "server_uuid", + "environment_name", + "dockerfile" + ], + "properties": { + "project_uuid": { + "type": "string", + "description": "The project UUID." + }, + "server_uuid": { + "type": "string", + "description": "The server UUID." + }, + "environment_name": { + "type": "string", + "description": "The environment name." + }, + "dockerfile": { + "type": "string", + "description": "The Dockerfile content." + }, + "build_pack": { + "type": "string", + "enum": [ + "nixpacks", + "static", + "dockerfile", + "dockercompose" + ], + "description": "The build pack type." + }, + "ports_exposes": { + "type": "string", + "description": "The ports to expose." + }, + "destination_uuid": { + "type": "string", + "description": "The destination UUID." + }, + "name": { + "type": "string", + "description": "The application name." + }, + "description": { + "type": "string", + "description": "The application description." + }, + "domains": { + "type": "string", + "description": "The application domains." + }, + "docker_registry_image_name": { + "type": "string", + "description": "The docker registry image name." + }, + "docker_registry_image_tag": { + "type": "string", + "description": "The docker registry image tag." + }, + "ports_mappings": { + "type": "string", + "description": "The ports mappings." + }, + "base_directory": { + "type": "string", + "description": "The base directory for all commands." + }, + "health_check_enabled": { + "type": "boolean", + "description": "Health check enabled." + }, + "health_check_path": { + "type": "string", + "description": "Health check path." + }, + "health_check_port": { + "type": "string", + "nullable": true, + "description": "Health check port." + }, + "health_check_host": { + "type": "string", + "nullable": true, + "description": "Health check host." + }, + "health_check_method": { + "type": "string", + "description": "Health check method." + }, + "health_check_return_code": { + "type": "integer", + "description": "Health check return code." + }, + "health_check_scheme": { + "type": "string", + "description": "Health check scheme." + }, + "health_check_response_text": { + "type": "string", + "nullable": true, + "description": "Health check response text." + }, + "health_check_interval": { + "type": "integer", + "description": "Health check interval in seconds." + }, + "health_check_timeout": { + "type": "integer", + "description": "Health check timeout in seconds." + }, + "health_check_retries": { + "type": "integer", + "description": "Health check retries count." + }, + "health_check_start_period": { + "type": "integer", + "description": "Health check start period in seconds." + }, + "limits_memory": { + "type": "string", + "description": "Memory limit." + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit." + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness." + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation." + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit." + }, + "limits_cpuset": { + "type": "string", + "nullable": true, + "description": "CPU set." + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares." + }, + "custom_labels": { + "type": "string", + "description": "Custom labels." + }, + "custom_docker_run_options": { + "type": "string", + "description": "Custom docker run options." + }, + "post_deployment_command": { + "type": "string", + "description": "Post deployment command." + }, + "post_deployment_command_container": { + "type": "string", + "description": "Post deployment command container." + }, + "pre_deployment_command": { + "type": "string", + "description": "Pre deployment command." + }, + "pre_deployment_command_container": { + "type": "string", + "description": "Pre deployment command container." + }, + "manual_webhook_secret_github": { + "type": "string", + "description": "Manual webhook secret for Github." + }, + "manual_webhook_secret_gitlab": { + "type": "string", + "description": "Manual webhook secret for Gitlab." + }, + "manual_webhook_secret_bitbucket": { + "type": "string", + "description": "Manual webhook secret for Bitbucket." + }, + "manual_webhook_secret_gitea": { + "type": "string", + "description": "Manual webhook secret for Gitea." + }, + "redirect": { + "type": "string", + "nullable": true, + "description": "How to set redirect with Traefik \/ Caddy. www<->non-www.", + "enum": [ + "www", + "non-www", + "both" + ] + }, + "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." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Application created successfully." + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/applications\/dockerimage": { + "post": { + "tags": [ + "Applications" + ], + "summary": "Create (Docker Image)", + "description": "Create new application based on a prebuilt docker image", + "operationId": "create-dockerimage-application", + "requestBody": { + "description": "Application object that needs to be created.", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "project_uuid", + "server_uuid", + "environment_name", + "docker_registry_image_name", + "ports_exposes" + ], + "properties": { + "project_uuid": { + "type": "string", + "description": "The project UUID." + }, + "server_uuid": { + "type": "string", + "description": "The server UUID." + }, + "environment_name": { + "type": "string", + "description": "The environment name." + }, + "docker_registry_image_name": { + "type": "string", + "description": "The docker registry image name." + }, + "docker_registry_image_tag": { + "type": "string", + "description": "The docker registry image tag." + }, + "ports_exposes": { + "type": "string", + "description": "The ports to expose." + }, + "destination_uuid": { + "type": "string", + "description": "The destination UUID." + }, + "name": { + "type": "string", + "description": "The application name." + }, + "description": { + "type": "string", + "description": "The application description." + }, + "domains": { + "type": "string", + "description": "The application domains." + }, + "ports_mappings": { + "type": "string", + "description": "The ports mappings." + }, + "health_check_enabled": { + "type": "boolean", + "description": "Health check enabled." + }, + "health_check_path": { + "type": "string", + "description": "Health check path." + }, + "health_check_port": { + "type": "string", + "nullable": true, + "description": "Health check port." + }, + "health_check_host": { + "type": "string", + "nullable": true, + "description": "Health check host." + }, + "health_check_method": { + "type": "string", + "description": "Health check method." + }, + "health_check_return_code": { + "type": "integer", + "description": "Health check return code." + }, + "health_check_scheme": { + "type": "string", + "description": "Health check scheme." + }, + "health_check_response_text": { + "type": "string", + "nullable": true, + "description": "Health check response text." + }, + "health_check_interval": { + "type": "integer", + "description": "Health check interval in seconds." + }, + "health_check_timeout": { + "type": "integer", + "description": "Health check timeout in seconds." + }, + "health_check_retries": { + "type": "integer", + "description": "Health check retries count." + }, + "health_check_start_period": { + "type": "integer", + "description": "Health check start period in seconds." + }, + "limits_memory": { + "type": "string", + "description": "Memory limit." + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit." + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness." + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation." + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit." + }, + "limits_cpuset": { + "type": "string", + "nullable": true, + "description": "CPU set." + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares." + }, + "custom_labels": { + "type": "string", + "description": "Custom labels." + }, + "custom_docker_run_options": { + "type": "string", + "description": "Custom docker run options." + }, + "post_deployment_command": { + "type": "string", + "description": "Post deployment command." + }, + "post_deployment_command_container": { + "type": "string", + "description": "Post deployment command container." + }, + "pre_deployment_command": { + "type": "string", + "description": "Pre deployment command." + }, + "pre_deployment_command_container": { + "type": "string", + "description": "Pre deployment command container." + }, + "manual_webhook_secret_github": { + "type": "string", + "description": "Manual webhook secret for Github." + }, + "manual_webhook_secret_gitlab": { + "type": "string", + "description": "Manual webhook secret for Gitlab." + }, + "manual_webhook_secret_bitbucket": { + "type": "string", + "description": "Manual webhook secret for Bitbucket." + }, + "manual_webhook_secret_gitea": { + "type": "string", + "description": "Manual webhook secret for Gitea." + }, + "redirect": { + "type": "string", + "nullable": true, + "description": "How to set redirect with Traefik \/ Caddy. www<->non-www.", + "enum": [ + "www", + "non-www", + "both" + ] + }, + "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." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Application created successfully." + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/applications\/dockercompose": { + "post": { + "tags": [ + "Applications" + ], + "summary": "Create (Docker Compose)", + "description": "Create new application based on a docker-compose file.", + "operationId": "create-dockercompose-application", + "requestBody": { + "description": "Application object that needs to be created.", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "project_uuid", + "server_uuid", + "environment_name", + "docker_compose_raw" + ], + "properties": { + "project_uuid": { + "type": "string", + "description": "The project UUID." + }, + "server_uuid": { + "type": "string", + "description": "The server UUID." + }, + "environment_name": { + "type": "string", + "description": "The environment name." + }, + "docker_compose_raw": { + "type": "string", + "description": "The Docker Compose raw content." + }, + "destination_uuid": { + "type": "string", + "description": "The destination UUID if the server has more than one destinations." + }, + "name": { + "type": "string", + "description": "The application name." + }, + "description": { + "type": "string", + "description": "The application description." + }, + "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." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Application created successfully." + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/applications\/{uuid}": { + "get": { + "tags": [ + "Applications" + ], + "summary": "Get", + "description": "Get application by UUID.", + "operationId": "get-application-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the application.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Get application by UUID.", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/Application" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "Applications" + ], + "summary": "Delete", + "description": "Delete application by UUID.", + "operationId": "delete-application-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the application.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "delete_configurations", + "in": "query", + "description": "Delete configurations.", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "delete_volumes", + "in": "query", + "description": "Delete volumes.", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "docker_cleanup", + "in": "query", + "description": "Run docker cleanup.", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "delete_connected_networks", + "in": "query", + "description": "Delete connected networks.", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Application deleted.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Application deleted." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "patch": { + "tags": [ + "Applications" + ], + "summary": "Update", + "description": "Update application by UUID.", + "operationId": "update-application-by-uuid", + "requestBody": { + "description": "Application updated.", + "required": true, + "content": { + "application\/json": { + "schema": { + "properties": { + "project_uuid": { + "type": "string", + "description": "The project UUID." + }, + "server_uuid": { + "type": "string", + "description": "The server UUID." + }, + "environment_name": { + "type": "string", + "description": "The environment name." + }, + "github_app_uuid": { + "type": "string", + "description": "The Github App UUID." + }, + "git_repository": { + "type": "string", + "description": "The git repository URL." + }, + "git_branch": { + "type": "string", + "description": "The git branch." + }, + "ports_exposes": { + "type": "string", + "description": "The ports to expose." + }, + "destination_uuid": { + "type": "string", + "description": "The destination UUID." + }, + "build_pack": { + "type": "string", + "enum": [ + "nixpacks", + "static", + "dockerfile", + "dockercompose" + ], + "description": "The build pack type." + }, + "name": { + "type": "string", + "description": "The application name." + }, + "description": { + "type": "string", + "description": "The application description." + }, + "domains": { + "type": "string", + "description": "The application domains." + }, + "git_commit_sha": { + "type": "string", + "description": "The git commit SHA." + }, + "docker_registry_image_name": { + "type": "string", + "description": "The docker registry image name." + }, + "docker_registry_image_tag": { + "type": "string", + "description": "The docker registry image tag." + }, + "is_static": { + "type": "boolean", + "description": "The flag to indicate if the application is static." + }, + "install_command": { + "type": "string", + "description": "The install command." + }, + "build_command": { + "type": "string", + "description": "The build command." + }, + "start_command": { + "type": "string", + "description": "The start command." + }, + "ports_mappings": { + "type": "string", + "description": "The ports mappings." + }, + "base_directory": { + "type": "string", + "description": "The base directory for all commands." + }, + "publish_directory": { + "type": "string", + "description": "The publish directory." + }, + "health_check_enabled": { + "type": "boolean", + "description": "Health check enabled." + }, + "health_check_path": { + "type": "string", + "description": "Health check path." + }, + "health_check_port": { + "type": "string", + "nullable": true, + "description": "Health check port." + }, + "health_check_host": { + "type": "string", + "nullable": true, + "description": "Health check host." + }, + "health_check_method": { + "type": "string", + "description": "Health check method." + }, + "health_check_return_code": { + "type": "integer", + "description": "Health check return code." + }, + "health_check_scheme": { + "type": "string", + "description": "Health check scheme." + }, + "health_check_response_text": { + "type": "string", + "nullable": true, + "description": "Health check response text." + }, + "health_check_interval": { + "type": "integer", + "description": "Health check interval in seconds." + }, + "health_check_timeout": { + "type": "integer", + "description": "Health check timeout in seconds." + }, + "health_check_retries": { + "type": "integer", + "description": "Health check retries count." + }, + "health_check_start_period": { + "type": "integer", + "description": "Health check start period in seconds." + }, + "limits_memory": { + "type": "string", + "description": "Memory limit." + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit." + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness." + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation." + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit." + }, + "limits_cpuset": { + "type": "string", + "nullable": true, + "description": "CPU set." + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares." + }, + "custom_labels": { + "type": "string", + "description": "Custom labels." + }, + "custom_docker_run_options": { + "type": "string", + "description": "Custom docker run options." + }, + "post_deployment_command": { + "type": "string", + "description": "Post deployment command." + }, + "post_deployment_command_container": { + "type": "string", + "description": "Post deployment command container." + }, + "pre_deployment_command": { + "type": "string", + "description": "Pre deployment command." + }, + "pre_deployment_command_container": { + "type": "string", + "description": "Pre deployment command container." + }, + "manual_webhook_secret_github": { + "type": "string", + "description": "Manual webhook secret for Github." + }, + "manual_webhook_secret_gitlab": { + "type": "string", + "description": "Manual webhook secret for Gitlab." + }, + "manual_webhook_secret_bitbucket": { + "type": "string", + "description": "Manual webhook secret for Bitbucket." + }, + "manual_webhook_secret_gitea": { + "type": "string", + "description": "Manual webhook secret for Gitea." + }, + "redirect": { + "type": "string", + "nullable": true, + "description": "How to set redirect with Traefik \/ Caddy. www<->non-www.", + "enum": [ + "www", + "non-www", + "both" + ] + }, + "instant_deploy": { + "type": "boolean", + "description": "The flag to indicate if the application should be deployed instantly." + }, + "dockerfile": { + "type": "string", + "description": "The Dockerfile content." + }, + "docker_compose_location": { + "type": "string", + "description": "The Docker Compose location." + }, + "docker_compose_raw": { + "type": "string", + "description": "The Docker Compose raw content." + }, + "docker_compose_custom_start_command": { + "type": "string", + "description": "The Docker Compose custom start command." + }, + "docker_compose_custom_build_command": { + "type": "string", + "description": "The Docker Compose custom build command." + }, + "docker_compose_domains": { + "type": "array", + "description": "The Docker Compose domains." + }, + "watch_paths": { + "type": "string", + "description": "The watch paths." + }, + "use_build_server": { + "type": "boolean", + "nullable": true, + "description": "Use build server." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Application updated.", + "content": { + "application\/json": { + "schema": { + "properties": { + "uuid": { + "type": "string" + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/applications\/{uuid}\/envs": { + "get": { + "tags": [ + "Applications" + ], + "summary": "List Envs", + "description": "List all envs by application UUID.", + "operationId": "list-envs-by-application-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the application.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "All environment variables by application UUID.", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/EnvironmentVariable" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "Applications" + ], + "summary": "Create Env", + "description": "Create env by application UUID.", + "operationId": "create-env-by-application-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the application.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "Env created.", + "required": true, + "content": { + "application\/json": { + "schema": { + "properties": { + "key": { + "type": "string", + "description": "The key of the environment variable." + }, + "value": { + "type": "string", + "description": "The value of the environment variable." + }, + "is_preview": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is used in preview deployments." + }, + "is_build_time": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is used in build time." + }, + "is_literal": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is a literal, nothing espaced." + }, + "is_multiline": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is multiline." + }, + "is_shown_once": { + "type": "boolean", + "description": "The flag to indicate if the environment variable's value is shown on the UI." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Environment variable created.", + "content": { + "application\/json": { + "schema": { + "properties": { + "uuid": { + "type": "string", + "example": "nc0k04gk8g0cgsk440g0koko" + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "patch": { + "tags": [ + "Applications" + ], + "summary": "Update Env", + "description": "Update env by application UUID.", + "operationId": "update-env-by-application-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the application.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "Env updated.", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string", + "description": "The key of the environment variable." + }, + "value": { + "type": "string", + "description": "The value of the environment variable." + }, + "is_preview": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is used in preview deployments." + }, + "is_build_time": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is used in build time." + }, + "is_literal": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is a literal, nothing espaced." + }, + "is_multiline": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is multiline." + }, + "is_shown_once": { + "type": "boolean", + "description": "The flag to indicate if the environment variable's value is shown on the UI." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Environment variable updated.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Environment variable updated." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/applications\/{uuid}\/envs\/bulk": { + "patch": { + "tags": [ + "Applications" + ], + "summary": "Update Envs (Bulk)", + "description": "Update multiple envs by application UUID.", + "operationId": "update-envs-by-application-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the application.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "Bulk envs updated.", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "data" + ], + "properties": { + "data": { + "type": "array", + "items": { + "properties": { + "key": { + "type": "string", + "description": "The key of the environment variable." + }, + "value": { + "type": "string", + "description": "The value of the environment variable." + }, + "is_preview": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is used in preview deployments." + }, + "is_build_time": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is used in build time." + }, + "is_literal": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is a literal, nothing espaced." + }, + "is_multiline": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is multiline." + }, + "is_shown_once": { + "type": "boolean", + "description": "The flag to indicate if the environment variable's value is shown on the UI." + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Environment variables updated.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Environment variables updated." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/applications\/{uuid}\/envs\/{env_uuid}": { + "delete": { + "tags": [ + "Applications" + ], + "summary": "Delete Env", + "description": "Delete env by UUID.", + "operationId": "delete-env-by-application-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the application.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "env_uuid", + "in": "path", + "description": "UUID of the environment variable.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Environment variable deleted.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Environment variable deleted." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/applications\/{uuid}\/start": { + "get": { + "tags": [ + "Applications" + ], + "summary": "Start", + "description": "Start application. `Post` request is also accepted.", + "operationId": "start-application-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the application.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "force", + "in": "query", + "description": "Force rebuild.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "instant_deploy", + "in": "query", + "description": "Instant deploy (skip queuing).", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Start application.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Deployment request queued.", + "description": "Message." + }, + "deployment_uuid": { + "type": "string", + "example": "doogksw", + "description": "UUID of the deployment." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/applications\/{uuid}\/stop": { + "get": { + "tags": [ + "Applications" + ], + "summary": "Stop", + "description": "Stop application. `Post` request is also accepted.", + "operationId": "stop-application-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the application.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Stop application.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Application stopping request queued." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/applications\/{uuid}\/restart": { + "get": { + "tags": [ + "Applications" + ], + "summary": "Restart", + "description": "Restart application. `Post` request is also accepted.", + "operationId": "restart-application-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the application.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Restart application.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Restart request queued." + }, + "deployment_uuid": { + "type": "string", + "example": "doogksw", + "description": "UUID of the deployment." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/applications\/{uuid}\/execute": { + "post": { + "tags": [ + "Applications" + ], + "summary": "Execute Command", + "description": "Execute a command on the application's current container.", + "operationId": "execute-command-application", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the application.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "Command to execute.", + "required": true, + "content": { + "application\/json": { + "schema": { + "properties": { + "command": { + "type": "string", + "description": "Command to execute." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Execute a command on the application's current container.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Command executed." + }, + "response": { + "type": "string" + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/databases": { + "get": { + "tags": [ + "Databases" + ], + "summary": "List", + "description": "List all databases.", + "operationId": "list-databases", + "responses": { + "200": { + "description": "Get all databases", + "content": { + "application\/json": { + "schema": { + "type": "string" + }, + "example": "Content is very complex. Will be implemented later." + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/databases\/{uuid}": { + "get": { + "tags": [ + "Databases" + ], + "summary": "Get", + "description": "Get database by UUID.", + "operationId": "get-database-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the database.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Get all databases", + "content": { + "application\/json": { + "schema": { + "type": "string" + }, + "example": "Content is very complex. Will be implemented later." + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "Databases" + ], + "summary": "Delete", + "description": "Delete database by UUID.", + "operationId": "delete-database-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the database.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "delete_configurations", + "in": "query", + "description": "Delete configurations.", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "delete_volumes", + "in": "query", + "description": "Delete volumes.", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "docker_cleanup", + "in": "query", + "description": "Run docker cleanup.", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "delete_connected_networks", + "in": "query", + "description": "Delete connected networks.", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Database deleted.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Database deleted." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "patch": { + "tags": [ + "Databases" + ], + "summary": "Update", + "description": "Update database by UUID.", + "operationId": "update-database-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the database.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "Database data", + "required": true, + "content": { + "application\/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "description": "Name of the database" + }, + "description": { + "type": "string", + "description": "Description of the database" + }, + "image": { + "type": "string", + "description": "Docker Image of the database" + }, + "is_public": { + "type": "boolean", + "description": "Is the database public?" + }, + "public_port": { + "type": "integer", + "description": "Public port of the database" + }, + "limits_memory": { + "type": "string", + "description": "Memory limit of the database" + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit of the database" + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness of the database" + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation of the database" + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit of the database" + }, + "limits_cpuset": { + "type": "string", + "description": "CPU set of the database" + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares of the database" + }, + "postgres_user": { + "type": "string", + "description": "PostgreSQL user" + }, + "postgres_password": { + "type": "string", + "description": "PostgreSQL password" + }, + "postgres_db": { + "type": "string", + "description": "PostgreSQL database" + }, + "postgres_initdb_args": { + "type": "string", + "description": "PostgreSQL initdb args" + }, + "postgres_host_auth_method": { + "type": "string", + "description": "PostgreSQL host auth method" + }, + "postgres_conf": { + "type": "string", + "description": "PostgreSQL conf" + }, + "clickhouse_admin_user": { + "type": "string", + "description": "Clickhouse admin user" + }, + "clickhouse_admin_password": { + "type": "string", + "description": "Clickhouse admin password" + }, + "dragonfly_password": { + "type": "string", + "description": "DragonFly password" + }, + "redis_password": { + "type": "string", + "description": "Redis password" + }, + "redis_conf": { + "type": "string", + "description": "Redis conf" + }, + "keydb_password": { + "type": "string", + "description": "KeyDB password" + }, + "keydb_conf": { + "type": "string", + "description": "KeyDB conf" + }, + "mariadb_conf": { + "type": "string", + "description": "MariaDB conf" + }, + "mariadb_root_password": { + "type": "string", + "description": "MariaDB root password" + }, + "mariadb_user": { + "type": "string", + "description": "MariaDB user" + }, + "mariadb_password": { + "type": "string", + "description": "MariaDB password" + }, + "mariadb_database": { + "type": "string", + "description": "MariaDB database" + }, + "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_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" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Database updated" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/databases\/postgresql": { + "post": { + "tags": [ + "Databases" + ], + "summary": "Create (PostgreSQL)", + "description": "Create a new PostgreSQL database.", + "operationId": "create-database-postgresql", + "requestBody": { + "description": "Database data", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "server_uuid", + "project_uuid", + "environment_name" + ], + "properties": { + "server_uuid": { + "type": "string", + "description": "UUID of the server" + }, + "project_uuid": { + "type": "string", + "description": "UUID of the project" + }, + "environment_name": { + "type": "string", + "description": "Name of the environment" + }, + "postgres_user": { + "type": "string", + "description": "PostgreSQL user" + }, + "postgres_password": { + "type": "string", + "description": "PostgreSQL password" + }, + "postgres_db": { + "type": "string", + "description": "PostgreSQL database" + }, + "postgres_initdb_args": { + "type": "string", + "description": "PostgreSQL initdb args" + }, + "postgres_host_auth_method": { + "type": "string", + "description": "PostgreSQL host auth method" + }, + "postgres_conf": { + "type": "string", + "description": "PostgreSQL conf" + }, + "destination_uuid": { + "type": "string", + "description": "UUID of the destination if the server has multiple destinations" + }, + "name": { + "type": "string", + "description": "Name of the database" + }, + "description": { + "type": "string", + "description": "Description of the database" + }, + "image": { + "type": "string", + "description": "Docker Image of the database" + }, + "is_public": { + "type": "boolean", + "description": "Is the database public?" + }, + "public_port": { + "type": "integer", + "description": "Public port of the database" + }, + "limits_memory": { + "type": "string", + "description": "Memory limit of the database" + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit of the database" + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness of the database" + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation of the database" + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit of the database" + }, + "limits_cpuset": { + "type": "string", + "description": "CPU set of the database" + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares of the database" + }, + "instant_deploy": { + "type": "boolean", + "description": "Instant deploy the database" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Database updated" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/databases\/clickhouse": { + "post": { + "tags": [ + "Databases" + ], + "summary": "Create (Clickhouse)", + "description": "Create a new Clickhouse database.", + "operationId": "create-database-clickhouse", + "requestBody": { + "description": "Database data", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "server_uuid", + "project_uuid", + "environment_name" + ], + "properties": { + "server_uuid": { + "type": "string", + "description": "UUID of the server" + }, + "project_uuid": { + "type": "string", + "description": "UUID of the project" + }, + "environment_name": { + "type": "string", + "description": "Name of the environment" + }, + "destination_uuid": { + "type": "string", + "description": "UUID of the destination if the server has multiple destinations" + }, + "clickhouse_admin_user": { + "type": "string", + "description": "Clickhouse admin user" + }, + "clickhouse_admin_password": { + "type": "string", + "description": "Clickhouse admin password" + }, + "name": { + "type": "string", + "description": "Name of the database" + }, + "description": { + "type": "string", + "description": "Description of the database" + }, + "image": { + "type": "string", + "description": "Docker Image of the database" + }, + "is_public": { + "type": "boolean", + "description": "Is the database public?" + }, + "public_port": { + "type": "integer", + "description": "Public port of the database" + }, + "limits_memory": { + "type": "string", + "description": "Memory limit of the database" + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit of the database" + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness of the database" + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation of the database" + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit of the database" + }, + "limits_cpuset": { + "type": "string", + "description": "CPU set of the database" + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares of the database" + }, + "instant_deploy": { + "type": "boolean", + "description": "Instant deploy the database" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Database updated" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/databases\/dragonfly": { + "post": { + "tags": [ + "Databases" + ], + "summary": "Create (DragonFly)", + "description": "Create a new DragonFly database.", + "operationId": "create-database-dragonfly", + "requestBody": { + "description": "Database data", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "server_uuid", + "project_uuid", + "environment_name" + ], + "properties": { + "server_uuid": { + "type": "string", + "description": "UUID of the server" + }, + "project_uuid": { + "type": "string", + "description": "UUID of the project" + }, + "environment_name": { + "type": "string", + "description": "Name of the environment" + }, + "destination_uuid": { + "type": "string", + "description": "UUID of the destination if the server has multiple destinations" + }, + "dragonfly_password": { + "type": "string", + "description": "DragonFly password" + }, + "name": { + "type": "string", + "description": "Name of the database" + }, + "description": { + "type": "string", + "description": "Description of the database" + }, + "image": { + "type": "string", + "description": "Docker Image of the database" + }, + "is_public": { + "type": "boolean", + "description": "Is the database public?" + }, + "public_port": { + "type": "integer", + "description": "Public port of the database" + }, + "limits_memory": { + "type": "string", + "description": "Memory limit of the database" + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit of the database" + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness of the database" + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation of the database" + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit of the database" + }, + "limits_cpuset": { + "type": "string", + "description": "CPU set of the database" + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares of the database" + }, + "instant_deploy": { + "type": "boolean", + "description": "Instant deploy the database" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Database updated" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/databases\/redis": { + "post": { + "tags": [ + "Databases" + ], + "summary": "Create (Redis)", + "description": "Create a new Redis database.", + "operationId": "create-database-redis", + "requestBody": { + "description": "Database data", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "server_uuid", + "project_uuid", + "environment_name" + ], + "properties": { + "server_uuid": { + "type": "string", + "description": "UUID of the server" + }, + "project_uuid": { + "type": "string", + "description": "UUID of the project" + }, + "environment_name": { + "type": "string", + "description": "Name of the environment" + }, + "destination_uuid": { + "type": "string", + "description": "UUID of the destination if the server has multiple destinations" + }, + "redis_password": { + "type": "string", + "description": "Redis password" + }, + "redis_conf": { + "type": "string", + "description": "Redis conf" + }, + "name": { + "type": "string", + "description": "Name of the database" + }, + "description": { + "type": "string", + "description": "Description of the database" + }, + "image": { + "type": "string", + "description": "Docker Image of the database" + }, + "is_public": { + "type": "boolean", + "description": "Is the database public?" + }, + "public_port": { + "type": "integer", + "description": "Public port of the database" + }, + "limits_memory": { + "type": "string", + "description": "Memory limit of the database" + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit of the database" + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness of the database" + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation of the database" + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit of the database" + }, + "limits_cpuset": { + "type": "string", + "description": "CPU set of the database" + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares of the database" + }, + "instant_deploy": { + "type": "boolean", + "description": "Instant deploy the database" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Database updated" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/databases\/keydb": { + "post": { + "tags": [ + "Databases" + ], + "summary": "Create (KeyDB)", + "description": "Create a new KeyDB database.", + "operationId": "create-database-keydb", + "requestBody": { + "description": "Database data", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "server_uuid", + "project_uuid", + "environment_name" + ], + "properties": { + "server_uuid": { + "type": "string", + "description": "UUID of the server" + }, + "project_uuid": { + "type": "string", + "description": "UUID of the project" + }, + "environment_name": { + "type": "string", + "description": "Name of the environment" + }, + "destination_uuid": { + "type": "string", + "description": "UUID of the destination if the server has multiple destinations" + }, + "keydb_password": { + "type": "string", + "description": "KeyDB password" + }, + "keydb_conf": { + "type": "string", + "description": "KeyDB conf" + }, + "name": { + "type": "string", + "description": "Name of the database" + }, + "description": { + "type": "string", + "description": "Description of the database" + }, + "image": { + "type": "string", + "description": "Docker Image of the database" + }, + "is_public": { + "type": "boolean", + "description": "Is the database public?" + }, + "public_port": { + "type": "integer", + "description": "Public port of the database" + }, + "limits_memory": { + "type": "string", + "description": "Memory limit of the database" + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit of the database" + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness of the database" + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation of the database" + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit of the database" + }, + "limits_cpuset": { + "type": "string", + "description": "CPU set of the database" + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares of the database" + }, + "instant_deploy": { + "type": "boolean", + "description": "Instant deploy the database" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Database updated" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/databases\/mariadb": { + "post": { + "tags": [ + "Databases" + ], + "summary": "Create (MariaDB)", + "description": "Create a new MariaDB database.", + "operationId": "create-database-mariadb", + "requestBody": { + "description": "Database data", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "server_uuid", + "project_uuid", + "environment_name" + ], + "properties": { + "server_uuid": { + "type": "string", + "description": "UUID of the server" + }, + "project_uuid": { + "type": "string", + "description": "UUID of the project" + }, + "environment_name": { + "type": "string", + "description": "Name of the environment" + }, + "destination_uuid": { + "type": "string", + "description": "UUID of the destination if the server has multiple destinations" + }, + "mariadb_conf": { + "type": "string", + "description": "MariaDB conf" + }, + "mariadb_root_password": { + "type": "string", + "description": "MariaDB root password" + }, + "mariadb_user": { + "type": "string", + "description": "MariaDB user" + }, + "mariadb_password": { + "type": "string", + "description": "MariaDB password" + }, + "mariadb_database": { + "type": "string", + "description": "MariaDB database" + }, + "name": { + "type": "string", + "description": "Name of the database" + }, + "description": { + "type": "string", + "description": "Description of the database" + }, + "image": { + "type": "string", + "description": "Docker Image of the database" + }, + "is_public": { + "type": "boolean", + "description": "Is the database public?" + }, + "public_port": { + "type": "integer", + "description": "Public port of the database" + }, + "limits_memory": { + "type": "string", + "description": "Memory limit of the database" + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit of the database" + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness of the database" + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation of the database" + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit of the database" + }, + "limits_cpuset": { + "type": "string", + "description": "CPU set of the database" + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares of the database" + }, + "instant_deploy": { + "type": "boolean", + "description": "Instant deploy the database" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Database updated" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/databases\/mysql": { + "post": { + "tags": [ + "Databases" + ], + "summary": "Create (MySQL)", + "description": "Create a new MySQL database.", + "operationId": "create-database-mysql", + "requestBody": { + "description": "Database data", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "server_uuid", + "project_uuid", + "environment_name" + ], + "properties": { + "server_uuid": { + "type": "string", + "description": "UUID of the server" + }, + "project_uuid": { + "type": "string", + "description": "UUID of the project" + }, + "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" + }, + "name": { + "type": "string", + "description": "Name of the database" + }, + "description": { + "type": "string", + "description": "Description of the database" + }, + "image": { + "type": "string", + "description": "Docker Image of the database" + }, + "is_public": { + "type": "boolean", + "description": "Is the database public?" + }, + "public_port": { + "type": "integer", + "description": "Public port of the database" + }, + "limits_memory": { + "type": "string", + "description": "Memory limit of the database" + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit of the database" + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness of the database" + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation of the database" + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit of the database" + }, + "limits_cpuset": { + "type": "string", + "description": "CPU set of the database" + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares of the database" + }, + "instant_deploy": { + "type": "boolean", + "description": "Instant deploy the database" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Database updated" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/databases\/mongodb": { + "post": { + "tags": [ + "Databases" + ], + "summary": "Create (MongoDB)", + "description": "Create a new MongoDB database.", + "operationId": "create-database-mongodb", + "requestBody": { + "description": "Database data", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "server_uuid", + "project_uuid", + "environment_name" + ], + "properties": { + "server_uuid": { + "type": "string", + "description": "UUID of the server" + }, + "project_uuid": { + "type": "string", + "description": "UUID of the project" + }, + "environment_name": { + "type": "string", + "description": "Name of the environment" + }, + "destination_uuid": { + "type": "string", + "description": "UUID of the destination if the server has multiple destinations" + }, + "mongo_conf": { + "type": "string", + "description": "MongoDB conf" + }, + "mongo_initdb_root_username": { + "type": "string", + "description": "MongoDB initdb root username" + }, + "name": { + "type": "string", + "description": "Name of the database" + }, + "description": { + "type": "string", + "description": "Description of the database" + }, + "image": { + "type": "string", + "description": "Docker Image of the database" + }, + "is_public": { + "type": "boolean", + "description": "Is the database public?" + }, + "public_port": { + "type": "integer", + "description": "Public port of the database" + }, + "limits_memory": { + "type": "string", + "description": "Memory limit of the database" + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit of the database" + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness of the database" + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation of the database" + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit of the database" + }, + "limits_cpuset": { + "type": "string", + "description": "CPU set of the database" + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares of the database" + }, + "instant_deploy": { + "type": "boolean", + "description": "Instant deploy the database" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Database updated" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/databases\/{uuid}\/start": { + "get": { + "tags": [ + "Databases" + ], + "summary": "Start", + "description": "Start database. `Post` request is also accepted.", + "operationId": "start-database-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the database.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Start database.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Database starting request queued." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/databases\/{uuid}\/stop": { + "get": { + "tags": [ + "Databases" + ], + "summary": "Stop", + "description": "Stop database. `Post` request is also accepted.", + "operationId": "stop-database-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the database.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Stop database.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Database stopping request queued." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/databases\/{uuid}\/restart": { + "get": { + "tags": [ + "Databases" + ], + "summary": "Restart", + "description": "Restart database. `Post` request is also accepted.", + "operationId": "restart-database-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the database.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Restart database.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Database restaring request queued." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/deployments": { + "get": { + "tags": [ + "Deployments" + ], + "summary": "List", + "description": "List currently running deployments", + "operationId": "list-deployments", + "responses": { + "200": { + "description": "Get all currently running deployments.", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/ApplicationDeploymentQueue" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/deployments\/{uuid}": { + "get": { + "tags": [ + "Deployments" + ], + "summary": "Get", + "description": "Get deployment by UUID.", + "operationId": "get-deployment-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Deployment UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Get deployment by UUID.", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/ApplicationDeploymentQueue" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/deploy": { + "get": { + "tags": [ + "Deployments" + ], + "summary": "Deploy", + "description": "Deploy by tag or uuid. `Post` request also accepted.", + "operationId": "deploy-by-tag-or-uuid", + "parameters": [ + { + "name": "tag", + "in": "query", + "description": "Tag name(s). Comma separated list is also accepted.", + "schema": { + "type": "string" + } + }, + { + "name": "uuid", + "in": "query", + "description": "Resource UUID(s). Comma separated list is also accepted.", + "schema": { + "type": "string" + } + }, + { + "name": "force", + "in": "query", + "description": "Force rebuild (without cache)", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Get deployment(s) UUID's", + "content": { + "application\/json": { + "schema": { + "properties": { + "deployments": { + "type": "array", + "items": { + "properties": { + "message": { + "type": "string" + }, + "resource_uuid": { + "type": "string" + }, + "deployment_uuid": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/version": { + "get": { + "summary": "Version", + "description": "Get Coolify version.", + "operationId": "version", + "responses": { + "200": { + "description": "Returns the version of the application", + "content": { + "application\/json": { + "schema": { + "type": "string" + }, + "example": "v4.0.0" + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/enable": { + "get": { + "summary": "Enable API", + "description": "Enable API (only with root permissions).", + "operationId": "enable-api", + "responses": { + "200": { + "description": "Enable API.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "API enabled." + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "You are not allowed to enable the API.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "You are not allowed to enable the API." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/disable": { + "get": { + "summary": "Disable API", + "description": "Disable API (only with root permissions).", + "operationId": "disable-api", + "responses": { + "200": { + "description": "Disable API.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "API disabled." + } + }, + "type": "object" + } + } + } + }, + "403": { + "description": "You are not allowed to disable the API.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "You are not allowed to disable the API." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/health": { + "get": { + "summary": "Healthcheck", + "description": "Healthcheck endpoint.", + "operationId": "healthcheck", + "responses": { + "200": { + "description": "Healthcheck endpoint.", + "content": { + "application\/json": { + "schema": { + "type": "string" + }, + "example": "OK" + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + } + } + }, + "\/projects": { + "get": { + "tags": [ + "Projects" + ], + "summary": "List", + "description": "List projects.", + "operationId": "list-projects", + "responses": { + "200": { + "description": "Get all projects.", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/Project" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "Projects" + ], + "summary": "Create", + "description": "Create Project.", + "operationId": "create-project", + "requestBody": { + "description": "Project created.", + "required": true, + "content": { + "application\/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "description": "The name of the project." + }, + "description": { + "type": "string", + "description": "The description of the project." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Project created.", + "content": { + "application\/json": { + "schema": { + "properties": { + "uuid": { + "type": "string", + "example": "og888os", + "description": "The UUID of the project." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/projects\/{uuid}": { + "get": { + "tags": [ + "Projects" + ], + "summary": "Get", + "description": "Get project by UUID.", + "operationId": "get-project-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Project UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project details", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/Project" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "description": "Project not found." + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "Projects" + ], + "summary": "Delete", + "description": "Delete project by UUID.", + "operationId": "delete-project-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the application.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Project deleted.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Project deleted." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "patch": { + "tags": [ + "Projects" + ], + "summary": "Update", + "description": "Update Project.", + "operationId": "update-project-by-uuid", + "requestBody": { + "description": "Project updated.", + "required": true, + "content": { + "application\/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "description": "The name of the project." + }, + "description": { + "type": "string", + "description": "The description of the project." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Project updated.", + "content": { + "application\/json": { + "schema": { + "properties": { + "uuid": { + "type": "string", + "example": "og888os" + }, + "name": { + "type": "string", + "example": "Project Name" + }, + "description": { + "type": "string", + "example": "Project Description" + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/projects\/{uuid}\/{environment_name}": { + "get": { + "tags": [ + "Projects" + ], + "summary": "Environment", + "description": "Get environment by name.", + "operationId": "get-environment-by-name", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Project UUID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "environment_name", + "in": "path", + "description": "Environment name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Environment details", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/Environment" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/resources": { + "get": { + "tags": [ + "Resources" + ], + "summary": "List", + "description": "Get all resources.", + "operationId": "list-resources", + "responses": { + "200": { + "description": "Get all resources", + "content": { + "application\/json": { + "schema": { + "type": "string" + }, + "example": "Content is very complex. Will be implemented later." + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/security\/keys": { + "get": { + "tags": [ + "Private Keys" + ], + "summary": "List", + "description": "List all private keys.", + "operationId": "list-private-keys", + "responses": { + "200": { + "description": "Get all private keys.", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/PrivateKey" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "Private Keys" + ], + "summary": "Create", + "description": "Create a new private key.", + "operationId": "create-private-key", + "requestBody": { + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "private_key" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "private_key": { + "type": "string" + } + }, + "type": "object", + "additionalProperties": false + } + } + } + }, + "responses": { + "201": { + "description": "The created private key's UUID.", + "content": { + "application\/json": { + "schema": { + "properties": { + "uuid": { + "type": "string" + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "patch": { + "tags": [ + "Private Keys" + ], + "summary": "Update", + "description": "Update a private key.", + "operationId": "update-private-key", + "requestBody": { + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "private_key" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "private_key": { + "type": "string" + } + }, + "type": "object", + "additionalProperties": false + } + } + } + }, + "responses": { + "201": { + "description": "The updated private key's UUID.", + "content": { + "application\/json": { + "schema": { + "properties": { + "uuid": { + "type": "string" + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/security\/keys\/{uuid}": { + "get": { + "tags": [ + "Private Keys" + ], + "summary": "Get", + "description": "Get key by UUID.", + "operationId": "get-private-key-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Private Key UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Get all private keys.", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/PrivateKey" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "description": "Private Key not found." + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "Private Keys" + ], + "summary": "Delete", + "description": "Delete a private key.", + "operationId": "delete-private-key-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Private Key UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Private Key deleted.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Private Key deleted." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "description": "Private Key not found." + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/servers": { + "get": { + "tags": [ + "Servers" + ], + "summary": "List", + "description": "List all servers.", + "operationId": "list-servers", + "responses": { + "200": { + "description": "Get all servers.", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/Server" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "Servers" + ], + "summary": "Create", + "description": "Create Server.", + "operationId": "create-server", + "requestBody": { + "description": "Server created.", + "required": true, + "content": { + "application\/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "example": "My Server", + "description": "The name of the server." + }, + "description": { + "type": "string", + "example": "My Server Description", + "description": "The description of the server." + }, + "ip": { + "type": "string", + "example": "127.0.0.1", + "description": "The IP of the server." + }, + "port": { + "type": "integer", + "example": 22, + "description": "The port of the server." + }, + "user": { + "type": "string", + "example": "root", + "description": "The user of the server." + }, + "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." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Server created.", + "content": { + "application\/json": { + "schema": { + "properties": { + "uuid": { + "type": "string", + "example": "og888os", + "description": "The UUID of the server." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/servers\/{uuid}": { + "get": { + "tags": [ + "Servers" + ], + "summary": "Get", + "description": "Get server by UUID.", + "operationId": "get-server-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Server's UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Get server by UUID", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/Server" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "Servers" + ], + "summary": "Delete", + "description": "Delete server by UUID.", + "operationId": "delete-server-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the server.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Server deleted.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Server deleted." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "patch": { + "tags": [ + "Servers" + ], + "summary": "Update", + "description": "Update Server.", + "operationId": "update-server-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Server UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Server updated.", + "required": true, + "content": { + "application\/json": { + "schema": { + "properties": { + "name": { + "type": "string", + "description": "The name of the server." + }, + "description": { + "type": "string", + "description": "The description of the server." + }, + "ip": { + "type": "string", + "description": "The IP of the server." + }, + "port": { + "type": "integer", + "description": "The port of the server." + }, + "user": { + "type": "string", + "description": "The user of the server." + }, + "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." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Server updated.", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/Server" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/servers\/{uuid}\/resources": { + "get": { + "tags": [ + "Servers" + ], + "summary": "Resources", + "description": "Get resources by server.", + "operationId": "get-resources-by-server-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Server's UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Get resources by server", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "properties": { + "id": { + "type": "integer" + }, + "uuid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "type": "object" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/servers\/{uuid}\/domains": { + "get": { + "tags": [ + "Servers" + ], + "summary": "Domains", + "description": "Get domains by server.", + "operationId": "get-domains-by-server-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Server's UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Get domains by server", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "properties": { + "ip": { + "type": "string" + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "type": "object" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/servers\/{uuid}\/validate": { + "get": { + "tags": [ + "Servers" + ], + "summary": "Validate", + "description": "Validate server by UUID.", + "operationId": "validate-server-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Server UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Server validation started.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Validation started." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/services": { + "get": { + "tags": [ + "Services" + ], + "summary": "List", + "description": "List all services.", + "operationId": "list-services", + "responses": { + "200": { + "description": "Get all services", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/Service" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "Services" + ], + "summary": "Create", + "description": "Create a one-click service", + "operationId": "create-service", + "requestBody": { + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "server_uuid", + "project_uuid", + "environment_name", + "type" + ], + "properties": { + "type": { + "description": "The one-click service type", + "type": "string", + "enum": [ + "activepieces", + "appsmith", + "appwrite", + "authentik", + "babybuddy", + "budge", + "changedetection", + "chatwoot", + "classicpress-with-mariadb", + "classicpress-with-mysql", + "classicpress-without-database", + "cloudflared", + "code-server", + "dashboard", + "directus", + "directus-with-postgresql", + "docker-registry", + "docuseal", + "docuseal-with-postgres", + "dokuwiki", + "duplicati", + "emby", + "embystat", + "fider", + "filebrowser", + "firefly", + "formbricks", + "ghost", + "gitea", + "gitea-with-mariadb", + "gitea-with-mysql", + "gitea-with-postgresql", + "glance", + "glances", + "glitchtip", + "grafana", + "grafana-with-postgresql", + "grocy", + "heimdall", + "homepage", + "jellyfin", + "kuzzle", + "listmonk", + "logto", + "mediawiki", + "meilisearch", + "metabase", + "metube", + "minio", + "moodle", + "n8n", + "n8n-with-postgresql", + "next-image-transformation", + "nextcloud", + "nocodb", + "odoo", + "openblocks", + "pairdrop", + "penpot", + "phpmyadmin", + "pocketbase", + "posthog", + "reactive-resume", + "rocketchat", + "shlink", + "slash", + "snapdrop", + "statusnook", + "stirling-pdf", + "supabase", + "syncthing", + "tolgee", + "trigger", + "trigger-with-external-database", + "twenty", + "umami", + "unleash-with-postgresql", + "unleash-without-database", + "uptime-kuma", + "vaultwarden", + "vikunja", + "weblate", + "whoogle", + "wordpress-with-mariadb", + "wordpress-with-mysql", + "wordpress-without-database" + ] + }, + "name": { + "type": "string", + "maxLength": 255, + "description": "Name of the service." + }, + "description": { + "type": "string", + "nullable": true, + "description": "Description of the service." + }, + "project_uuid": { + "type": "string", + "description": "Project UUID." + }, + "environment_name": { + "type": "string", + "description": "Environment name." + }, + "server_uuid": { + "type": "string", + "description": "Server UUID." + }, + "destination_uuid": { + "type": "string", + "description": "Destination UUID. Required if server has multiple destinations." + }, + "instant_deploy": { + "type": "boolean", + "default": false, + "description": "Start the service immediately after creation." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Create a service.", + "content": { + "application\/json": { + "schema": { + "properties": { + "uuid": { + "type": "string", + "description": "Service UUID." + }, + "domains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Service domains." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/services\/{uuid}": { + "get": { + "tags": [ + "Services" + ], + "summary": "Get", + "description": "Get service by UUID.", + "operationId": "get-service-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Service UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Get a service by UUID.", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/Service" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": [ + "Services" + ], + "summary": "Delete", + "description": "Delete service by UUID.", + "operationId": "delete-service-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "Service UUID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "delete_configurations", + "in": "query", + "description": "Delete configurations.", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "delete_volumes", + "in": "query", + "description": "Delete volumes.", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "docker_cleanup", + "in": "query", + "description": "Run docker cleanup.", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "delete_connected_networks", + "in": "query", + "description": "Delete connected networks.", + "required": false, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Delete a service by UUID", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Service deletion request queued." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/services\/{uuid}\/envs": { + "get": { + "tags": [ + "Services" + ], + "summary": "List Envs", + "description": "List all envs by service UUID.", + "operationId": "list-envs-by-service-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the service.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "All environment variables by service UUID.", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/EnvironmentVariable" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": [ + "Services" + ], + "summary": "Create Env", + "description": "Create env by service UUID.", + "operationId": "create-env-by-service-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the service.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "Env created.", + "required": true, + "content": { + "application\/json": { + "schema": { + "properties": { + "key": { + "type": "string", + "description": "The key of the environment variable." + }, + "value": { + "type": "string", + "description": "The value of the environment variable." + }, + "is_preview": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is used in preview deployments." + }, + "is_build_time": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is used in build time." + }, + "is_literal": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is a literal, nothing espaced." + }, + "is_multiline": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is multiline." + }, + "is_shown_once": { + "type": "boolean", + "description": "The flag to indicate if the environment variable's value is shown on the UI." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Environment variable created.", + "content": { + "application\/json": { + "schema": { + "properties": { + "uuid": { + "type": "string", + "example": "nc0k04gk8g0cgsk440g0koko" + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "patch": { + "tags": [ + "Services" + ], + "summary": "Update Env", + "description": "Update env by service UUID.", + "operationId": "update-env-by-service-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the service.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "Env updated.", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string", + "description": "The key of the environment variable." + }, + "value": { + "type": "string", + "description": "The value of the environment variable." + }, + "is_preview": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is used in preview deployments." + }, + "is_build_time": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is used in build time." + }, + "is_literal": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is a literal, nothing espaced." + }, + "is_multiline": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is multiline." + }, + "is_shown_once": { + "type": "boolean", + "description": "The flag to indicate if the environment variable's value is shown on the UI." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Environment variable updated.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Environment variable updated." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/services\/{uuid}\/envs\/bulk": { + "patch": { + "tags": [ + "Services" + ], + "summary": "Update Envs (Bulk)", + "description": "Update multiple envs by service UUID.", + "operationId": "update-envs-by-service-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the service.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "description": "Bulk envs updated.", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "data" + ], + "properties": { + "data": { + "type": "array", + "items": { + "properties": { + "key": { + "type": "string", + "description": "The key of the environment variable." + }, + "value": { + "type": "string", + "description": "The value of the environment variable." + }, + "is_preview": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is used in preview deployments." + }, + "is_build_time": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is used in build time." + }, + "is_literal": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is a literal, nothing espaced." + }, + "is_multiline": { + "type": "boolean", + "description": "The flag to indicate if the environment variable is multiline." + }, + "is_shown_once": { + "type": "boolean", + "description": "The flag to indicate if the environment variable's value is shown on the UI." + } + }, + "type": "object" + } + } + }, + "type": "object" + } + } + } + }, + "responses": { + "201": { + "description": "Environment variables updated.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Environment variables updated." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/services\/{uuid}\/envs\/{env_uuid}": { + "delete": { + "tags": [ + "Services" + ], + "summary": "Delete Env", + "description": "Delete env by UUID.", + "operationId": "delete-env-by-service-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the service.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "env_uuid", + "in": "path", + "description": "UUID of the environment variable.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Environment variable deleted.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Environment variable deleted." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/services\/{uuid}\/start": { + "get": { + "tags": [ + "Services" + ], + "summary": "Start", + "description": "Start service. `Post` request is also accepted.", + "operationId": "start-service-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the service.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Start service.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Service starting request queued." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/services\/{uuid}\/stop": { + "get": { + "tags": [ + "Services" + ], + "summary": "Stop", + "description": "Stop service. `Post` request is also accepted.", + "operationId": "stop-service-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the service.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Stop service.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Service stopping request queued." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/services\/{uuid}\/restart": { + "get": { + "tags": [ + "Services" + ], + "summary": "Restart", + "description": "Restart service. `Post` request is also accepted.", + "operationId": "restart-service-by-uuid", + "parameters": [ + { + "name": "uuid", + "in": "path", + "description": "UUID of the service.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Restart service.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Service restaring request queued." + } + }, + "type": "object" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/teams": { + "get": { + "tags": [ + "Teams" + ], + "summary": "List", + "description": "Get all teams.", + "operationId": "list-teams", + "responses": { + "200": { + "description": "List of teams.", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/Team" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/teams\/{id}": { + "get": { + "tags": [ + "Teams" + ], + "summary": "Get", + "description": "Get team by TeamId.", + "operationId": "get-team-by-id", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Team ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "List of teams.", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/Team" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/teams\/{id}\/members": { + "get": { + "tags": [ + "Teams" + ], + "summary": "Members", + "description": "Get members by TeamId.", + "operationId": "get-members-by-team-id", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Team ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "List of members.", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/User" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "404": { + "$ref": "#\/components\/responses\/404" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/teams\/current": { + "get": { + "tags": [ + "Teams" + ], + "summary": "Authenticated Team", + "description": "Get currently authenticated team.", + "operationId": "get-current-team", + "responses": { + "200": { + "description": "Current Team.", + "content": { + "application\/json": { + "schema": { + "$ref": "#\/components\/schemas\/Team" + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "\/teams\/current\/members": { + "get": { + "tags": [ + "Teams" + ], + "summary": "Authenticated Team Members", + "description": "Get currently authenticated team members.", + "operationId": "get-current-team-members", + "responses": { + "200": { + "description": "Currently authenticated team members.", + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/User" + } + } + } + } + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + } + }, + "components": { + "schemas": { + "Application": { + "description": "Application model", + "properties": { + "id": { + "type": "integer", + "description": "The application identifier in the database." + }, + "description": { + "type": "string", + "nullable": true, + "description": "The application description." + }, + "repository_project_id": { + "type": "integer", + "nullable": true, + "description": "The repository project identifier." + }, + "uuid": { + "type": "string", + "description": "The application UUID." + }, + "name": { + "type": "string", + "description": "The application name." + }, + "fqdn": { + "type": "string", + "nullable": true, + "description": "The application domains." + }, + "config_hash": { + "type": "string", + "description": "Configuration hash." + }, + "git_repository": { + "type": "string", + "description": "Git repository URL." + }, + "git_branch": { + "type": "string", + "description": "Git branch." + }, + "git_commit_sha": { + "type": "string", + "description": "Git commit SHA." + }, + "git_full_url": { + "type": "string", + "nullable": true, + "description": "Git full URL." + }, + "docker_registry_image_name": { + "type": "string", + "nullable": true, + "description": "Docker registry image name." + }, + "docker_registry_image_tag": { + "type": "string", + "nullable": true, + "description": "Docker registry image tag." + }, + "build_pack": { + "type": "string", + "description": "Build pack.", + "enum": [ + "nixpacks", + "static", + "dockerfile", + "dockercompose" + ] + }, + "static_image": { + "type": "string", + "description": "Static image used when static site is deployed." + }, + "install_command": { + "type": "string", + "description": "Install command." + }, + "build_command": { + "type": "string", + "description": "Build command." + }, + "start_command": { + "type": "string", + "description": "Start command." + }, + "ports_exposes": { + "type": "string", + "description": "Ports exposes." + }, + "ports_mappings": { + "type": "string", + "nullable": true, + "description": "Ports mappings." + }, + "base_directory": { + "type": "string", + "description": "Base directory for all commands." + }, + "publish_directory": { + "type": "string", + "description": "Publish directory." + }, + "health_check_enabled": { + "type": "boolean", + "description": "Health check enabled." + }, + "health_check_path": { + "type": "string", + "description": "Health check path." + }, + "health_check_port": { + "type": "string", + "nullable": true, + "description": "Health check port." + }, + "health_check_host": { + "type": "string", + "nullable": true, + "description": "Health check host." + }, + "health_check_method": { + "type": "string", + "description": "Health check method." + }, + "health_check_return_code": { + "type": "integer", + "description": "Health check return code." + }, + "health_check_scheme": { + "type": "string", + "description": "Health check scheme." + }, + "health_check_response_text": { + "type": "string", + "nullable": true, + "description": "Health check response text." + }, + "health_check_interval": { + "type": "integer", + "description": "Health check interval in seconds." + }, + "health_check_timeout": { + "type": "integer", + "description": "Health check timeout in seconds." + }, + "health_check_retries": { + "type": "integer", + "description": "Health check retries count." + }, + "health_check_start_period": { + "type": "integer", + "description": "Health check start period in seconds." + }, + "limits_memory": { + "type": "string", + "description": "Memory limit." + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit." + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness." + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation." + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit." + }, + "limits_cpuset": { + "type": "string", + "nullable": true, + "description": "CPU set." + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares." + }, + "status": { + "type": "string", + "description": "Application status." + }, + "preview_url_template": { + "type": "string", + "description": "Preview URL template." + }, + "destination_type": { + "type": "string", + "description": "Destination type." + }, + "destination_id": { + "type": "integer", + "description": "Destination identifier." + }, + "source_id": { + "type": "integer", + "nullable": true, + "description": "Source identifier." + }, + "private_key_id": { + "type": "integer", + "nullable": true, + "description": "Private key identifier." + }, + "environment_id": { + "type": "integer", + "description": "Environment identifier." + }, + "dockerfile": { + "type": "string", + "nullable": true, + "description": "Dockerfile content. Used for dockerfile build pack." + }, + "dockerfile_location": { + "type": "string", + "description": "Dockerfile location." + }, + "custom_labels": { + "type": "string", + "nullable": true, + "description": "Custom labels." + }, + "dockerfile_target_build": { + "type": "string", + "nullable": true, + "description": "Dockerfile target build." + }, + "manual_webhook_secret_github": { + "type": "string", + "nullable": true, + "description": "Manual webhook secret for GitHub." + }, + "manual_webhook_secret_gitlab": { + "type": "string", + "nullable": true, + "description": "Manual webhook secret for GitLab." + }, + "manual_webhook_secret_bitbucket": { + "type": "string", + "nullable": true, + "description": "Manual webhook secret for Bitbucket." + }, + "manual_webhook_secret_gitea": { + "type": "string", + "nullable": true, + "description": "Manual webhook secret for Gitea." + }, + "docker_compose_location": { + "type": "string", + "description": "Docker compose location." + }, + "docker_compose": { + "type": "string", + "nullable": true, + "description": "Docker compose content. Used for docker compose build pack." + }, + "docker_compose_raw": { + "type": "string", + "nullable": true, + "description": "Docker compose raw content." + }, + "docker_compose_domains": { + "type": "string", + "nullable": true, + "description": "Docker compose domains." + }, + "docker_compose_custom_start_command": { + "type": "string", + "nullable": true, + "description": "Docker compose custom start command." + }, + "docker_compose_custom_build_command": { + "type": "string", + "nullable": true, + "description": "Docker compose custom build command." + }, + "swarm_replicas": { + "type": "integer", + "nullable": true, + "description": "Swarm replicas. Only used for swarm deployments." + }, + "swarm_placement_constraints": { + "type": "string", + "nullable": true, + "description": "Swarm placement constraints. Only used for swarm deployments." + }, + "custom_docker_run_options": { + "type": "string", + "nullable": true, + "description": "Custom docker run options." + }, + "post_deployment_command": { + "type": "string", + "nullable": true, + "description": "Post deployment command." + }, + "post_deployment_command_container": { + "type": "string", + "nullable": true, + "description": "Post deployment command container." + }, + "pre_deployment_command": { + "type": "string", + "nullable": true, + "description": "Pre deployment command." + }, + "pre_deployment_command_container": { + "type": "string", + "nullable": true, + "description": "Pre deployment command container." + }, + "watch_paths": { + "type": "string", + "nullable": true, + "description": "Watch paths." + }, + "custom_healthcheck_found": { + "type": "boolean", + "description": "Custom healthcheck found." + }, + "redirect": { + "type": "string", + "nullable": true, + "description": "How to set redirect with Traefik \/ Caddy. www<->non-www.", + "enum": [ + "www", + "non-www", + "both" + ] + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "The date and time when the application was created." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "The date and time when the application was last updated." + }, + "deleted_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "The date and time when the application was deleted." + }, + "compose_parsing_version": { + "type": "string", + "description": "How Coolify parse the compose file." + }, + "custom_nginx_configuration": { + "type": "string", + "nullable": true, + "description": "Custom Nginx configuration base64 encoded." + } + }, + "type": "object" + }, + "ApplicationDeploymentQueue": { + "description": "Project model", + "properties": { + "id": { + "type": "integer" + }, + "application_id": { + "type": "string" + }, + "deployment_uuid": { + "type": "string" + }, + "pull_request_id": { + "type": "integer" + }, + "force_rebuild": { + "type": "boolean" + }, + "commit": { + "type": "string" + }, + "status": { + "type": "string" + }, + "is_webhook": { + "type": "boolean" + }, + "is_api": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "logs": { + "type": "string" + }, + "current_process_id": { + "type": "string" + }, + "restart_only": { + "type": "boolean" + }, + "git_type": { + "type": "string" + }, + "server_id": { + "type": "integer" + }, + "application_name": { + "type": "string" + }, + "server_name": { + "type": "string" + }, + "deployment_url": { + "type": "string" + }, + "destination_id": { + "type": "string" + }, + "only_this_server": { + "type": "boolean" + }, + "rollback": { + "type": "boolean" + }, + "commit_message": { + "type": "string" + } + }, + "type": "object" + }, + "Environment": { + "description": "Environment model", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "project_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "type": "object" + }, + "EnvironmentVariable": { + "description": "Environment Variable model", + "properties": { + "id": { + "type": "integer" + }, + "uuid": { + "type": "string" + }, + "application_id": { + "type": "integer" + }, + "service_id": { + "type": "integer" + }, + "database_id": { + "type": "integer" + }, + "is_build_time": { + "type": "boolean" + }, + "is_literal": { + "type": "boolean" + }, + "is_multiline": { + "type": "boolean" + }, + "is_preview": { + "type": "boolean" + }, + "is_shared": { + "type": "boolean" + }, + "is_shown_once": { + "type": "boolean" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + }, + "real_value": { + "type": "string" + }, + "version": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "type": "object" + }, + "PrivateKey": { + "description": "Private Key model", + "properties": { + "id": { + "type": "integer" + }, + "uuid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "private_key": { + "type": "string", + "format": "private-key" + }, + "is_git_related": { + "type": "boolean" + }, + "team_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "type": "object" + }, + "Project": { + "description": "Project model", + "properties": { + "id": { + "type": "integer" + }, + "uuid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "environments": { + "description": "The environments of the project.", + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/Environment" + } + } + }, + "type": "object" + }, + "Server": { + "description": "Server model", + "properties": { + "id": { + "type": "integer", + "description": "The server ID." + }, + "uuid": { + "type": "string", + "description": "The server UUID." + }, + "name": { + "type": "string", + "description": "The server name." + }, + "description": { + "type": "string", + "description": "The server description." + }, + "ip": { + "type": "string", + "description": "The IP address." + }, + "user": { + "type": "string", + "description": "The user." + }, + "port": { + "type": "integer", + "description": "The port number." + }, + "proxy": { + "type": "object", + "description": "The proxy configuration." + }, + "proxy_type": { + "type": "string", + "enum": [ + "traefik", + "caddy", + "none" + ], + "description": "The proxy type." + }, + "high_disk_usage_notification_sent": { + "type": "boolean", + "description": "The flag to indicate if the high disk usage notification has been sent." + }, + "unreachable_notification_sent": { + "type": "boolean", + "description": "The flag to indicate if the unreachable notification has been sent." + }, + "unreachable_count": { + "type": "integer", + "description": "The unreachable count for your server." + }, + "validation_logs": { + "type": "string", + "description": "The validation logs." + }, + "log_drain_notification_sent": { + "type": "boolean", + "description": "The flag to indicate if the log drain notification has been sent." + }, + "swarm_cluster": { + "type": "string", + "description": "The swarm cluster configuration." + }, + "settings": { + "$ref": "#\/components\/schemas\/ServerSetting" + } + }, + "type": "object" + }, + "ServerSetting": { + "description": "Server Settings model", + "properties": { + "id": { + "type": "integer" + }, + "concurrent_builds": { + "type": "integer" + }, + "dynamic_timeout": { + "type": "integer" + }, + "force_disabled": { + "type": "boolean" + }, + "force_server_cleanup": { + "type": "boolean" + }, + "is_build_server": { + "type": "boolean" + }, + "is_cloudflare_tunnel": { + "type": "boolean" + }, + "is_jump_server": { + "type": "boolean" + }, + "is_logdrain_axiom_enabled": { + "type": "boolean" + }, + "is_logdrain_custom_enabled": { + "type": "boolean" + }, + "is_logdrain_highlight_enabled": { + "type": "boolean" + }, + "is_logdrain_newrelic_enabled": { + "type": "boolean" + }, + "is_metrics_enabled": { + "type": "boolean" + }, + "is_reachable": { + "type": "boolean" + }, + "is_sentinel_enabled": { + "type": "boolean" + }, + "is_swarm_manager": { + "type": "boolean" + }, + "is_swarm_worker": { + "type": "boolean" + }, + "is_usable": { + "type": "boolean" + }, + "logdrain_axiom_api_key": { + "type": "string" + }, + "logdrain_axiom_dataset_name": { + "type": "string" + }, + "logdrain_custom_config": { + "type": "string" + }, + "logdrain_custom_config_parser": { + "type": "string" + }, + "logdrain_highlight_project_id": { + "type": "string" + }, + "logdrain_newrelic_base_uri": { + "type": "string" + }, + "logdrain_newrelic_license_key": { + "type": "string" + }, + "sentinel_metrics_history_days": { + "type": "integer" + }, + "sentinel_metrics_refresh_rate_seconds": { + "type": "integer" + }, + "sentinel_token": { + "type": "string" + }, + "docker_cleanup_frequency": { + "type": "string" + }, + "docker_cleanup_threshold": { + "type": "integer" + }, + "server_id": { + "type": "integer" + }, + "wildcard_domain": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "delete_unused_volumes": { + "type": "boolean", + "description": "The flag to indicate if the unused volumes should be deleted." + }, + "delete_unused_networks": { + "type": "boolean", + "description": "The flag to indicate if the unused networks should be deleted." + } + }, + "type": "object" + }, + "Service": { + "description": "Service model", + "properties": { + "id": { + "type": "integer", + "description": "The unique identifier of the service. Only used for database identification." + }, + "uuid": { + "type": "string", + "description": "The unique identifier of the service." + }, + "name": { + "type": "string", + "description": "The name of the service." + }, + "environment_id": { + "type": "integer", + "description": "The unique identifier of the environment where the service is attached to." + }, + "server_id": { + "type": "integer", + "description": "The unique identifier of the server where the service is running." + }, + "description": { + "type": "string", + "description": "The description of the service." + }, + "docker_compose_raw": { + "type": "string", + "description": "The raw docker-compose.yml file of the service." + }, + "docker_compose": { + "type": "string", + "description": "The docker-compose.yml file that is parsed and modified by Coolify." + }, + "destination_type": { + "type": "string", + "description": "Destination type." + }, + "destination_id": { + "type": "integer", + "description": "The unique identifier of the destination where the service is running." + }, + "connect_to_docker_network": { + "type": "boolean", + "description": "The flag to connect the service to the predefined Docker network." + }, + "is_container_label_escape_enabled": { + "type": "boolean", + "description": "The flag to enable the container label escape." + }, + "is_container_label_readonly_enabled": { + "type": "boolean", + "description": "The flag to enable the container label readonly." + }, + "config_hash": { + "type": "string", + "description": "The hash of the service configuration." + }, + "service_type": { + "type": "string", + "description": "The type of the service." + }, + "created_at": { + "type": "string", + "description": "The date and time when the service was created." + }, + "updated_at": { + "type": "string", + "description": "The date and time when the service was last updated." + }, + "deleted_at": { + "type": "string", + "description": "The date and time when the service was deleted." + } + }, + "type": "object" + }, + "Team": { + "description": "Team model", + "properties": { + "id": { + "type": "integer", + "description": "The unique identifier of the team." + }, + "name": { + "type": "string", + "description": "The name of the team." + }, + "description": { + "type": "string", + "description": "The description of the team." + }, + "personal_team": { + "type": "boolean", + "description": "Whether the team is personal or not." + }, + "created_at": { + "type": "string", + "description": "The date and time the team was created." + }, + "updated_at": { + "type": "string", + "description": "The date and time the team was last updated." + }, + "smtp_enabled": { + "type": "boolean", + "description": "Whether SMTP is enabled or not." + }, + "smtp_from_address": { + "type": "string", + "description": "The email address to send emails from." + }, + "smtp_from_name": { + "type": "string", + "description": "The name to send emails from." + }, + "smtp_recipients": { + "type": "string", + "description": "The email addresses to send emails to." + }, + "smtp_host": { + "type": "string", + "description": "The SMTP host." + }, + "smtp_port": { + "type": "string", + "description": "The SMTP port." + }, + "smtp_encryption": { + "type": "string", + "description": "The SMTP encryption." + }, + "smtp_username": { + "type": "string", + "description": "The SMTP username." + }, + "smtp_password": { + "type": "string", + "description": "The SMTP password." + }, + "smtp_timeout": { + "type": "string", + "description": "The SMTP timeout." + }, + "smtp_notifications_test": { + "type": "boolean", + "description": "Whether to send test notifications via SMTP." + }, + "smtp_notifications_deployments": { + "type": "boolean", + "description": "Whether to send deployment notifications via SMTP." + }, + "smtp_notifications_status_changes": { + "type": "boolean", + "description": "Whether to send status change notifications via SMTP." + }, + "smtp_notifications_scheduled_tasks": { + "type": "boolean", + "description": "Whether to send scheduled task notifications via SMTP." + }, + "smtp_notifications_database_backups": { + "type": "boolean", + "description": "Whether to send database backup notifications via SMTP." + }, + "smtp_notifications_server_disk_usage": { + "type": "boolean", + "description": "Whether to send server disk usage notifications via SMTP." + }, + "discord_enabled": { + "type": "boolean", + "description": "Whether Discord is enabled or not." + }, + "discord_webhook_url": { + "type": "string", + "description": "The Discord webhook URL." + }, + "discord_notifications_test": { + "type": "boolean", + "description": "Whether to send test notifications via Discord." + }, + "discord_notifications_deployments": { + "type": "boolean", + "description": "Whether to send deployment notifications via Discord." + }, + "discord_notifications_status_changes": { + "type": "boolean", + "description": "Whether to send status change notifications via Discord." + }, + "discord_notifications_database_backups": { + "type": "boolean", + "description": "Whether to send database backup notifications via Discord." + }, + "discord_notifications_scheduled_tasks": { + "type": "boolean", + "description": "Whether to send scheduled task notifications via Discord." + }, + "discord_notifications_server_disk_usage": { + "type": "boolean", + "description": "Whether to send server disk usage notifications via Discord." + }, + "show_boarding": { + "type": "boolean", + "description": "Whether to show the boarding screen or not." + }, + "resend_enabled": { + "type": "boolean", + "description": "Whether to enable resending or not." + }, + "resend_api_key": { + "type": "string", + "description": "The resending API key." + }, + "use_instance_email_settings": { + "type": "boolean", + "description": "Whether to use instance email settings or not." + }, + "telegram_enabled": { + "type": "boolean", + "description": "Whether Telegram is enabled or not." + }, + "telegram_token": { + "type": "string", + "description": "The Telegram token." + }, + "telegram_chat_id": { + "type": "string", + "description": "The Telegram chat ID." + }, + "telegram_notifications_test": { + "type": "boolean", + "description": "Whether to send test notifications via Telegram." + }, + "telegram_notifications_deployments": { + "type": "boolean", + "description": "Whether to send deployment notifications via Telegram." + }, + "telegram_notifications_status_changes": { + "type": "boolean", + "description": "Whether to send status change notifications via Telegram." + }, + "telegram_notifications_database_backups": { + "type": "boolean", + "description": "Whether to send database backup notifications via Telegram." + }, + "telegram_notifications_test_message_thread_id": { + "type": "string", + "description": "The Telegram test message thread ID." + }, + "telegram_notifications_deployments_message_thread_id": { + "type": "string", + "description": "The Telegram deployment message thread ID." + }, + "telegram_notifications_status_changes_message_thread_id": { + "type": "string", + "description": "The Telegram status change message thread ID." + }, + "telegram_notifications_database_backups_message_thread_id": { + "type": "string", + "description": "The Telegram database backup message thread ID." + }, + "custom_server_limit": { + "type": "string", + "description": "The custom server limit." + }, + "telegram_notifications_scheduled_tasks": { + "type": "boolean", + "description": "Whether to send scheduled task notifications via Telegram." + }, + "telegram_notifications_scheduled_tasks_thread_id": { + "type": "string", + "description": "The Telegram scheduled task message thread ID." + }, + "members": { + "description": "The members of the team.", + "type": "array", + "items": { + "$ref": "#\/components\/schemas\/User" + } + } + }, + "type": "object" + }, + "User": { + "description": "User model", + "properties": { + "id": { + "type": "integer", + "description": "The user identifier in the database." + }, + "name": { + "type": "string", + "description": "The user name." + }, + "email": { + "type": "string", + "description": "The user email." + }, + "email_verified_at": { + "type": "string", + "description": "The date when the user email was verified." + }, + "created_at": { + "type": "string", + "description": "The date when the user was created." + }, + "updated_at": { + "type": "string", + "description": "The date when the user was updated." + }, + "two_factor_confirmed_at": { + "type": "string", + "description": "The date when the user two factor was confirmed." + }, + "force_password_reset": { + "type": "boolean", + "description": "The flag to force the user to reset the password." + }, + "marketing_emails": { + "type": "boolean", + "description": "The flag to receive marketing emails." + } + }, + "type": "object" + } + }, + "responses": { + "400": { + "description": "Invalid token.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Invalid token." + } + }, + "type": "object" + } + } + } + }, + "401": { + "description": "Unauthenticated.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Unauthenticated." + } + }, + "type": "object" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Resource not found." + } + }, + "type": "object" + } + } + } + } + }, + "securitySchemes": { + "bearerAuth": { + "type": "http", + "description": "Go to `Keys & Tokens` \/ `API tokens` and create a new token. Use the token as the bearer token.", + "scheme": "bearer" + } + } + }, + "tags": [ + { + "name": "Applications", + "description": "Applications" + }, + { + "name": "Databases", + "description": "Databases" + }, + { + "name": "Deployments", + "description": "Deployments" + }, + { + "name": "Projects", + "description": "Projects" + }, + { + "name": "Resources", + "description": "Resources" + }, + { + "name": "Private Keys", + "description": "Private Keys" + }, + { + "name": "Servers", + "description": "Servers" + }, + { + "name": "Services", + "description": "Services" + }, + { + "name": "Teams", + "description": "Teams" + } + ] +} \ No newline at end of file diff --git a/openapi.yaml b/openapi.yaml index d2616e9c6..20bf34873 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.0 +openapi: 3.1.0 info: title: Coolify version: '0.1' @@ -2089,12 +2089,15 @@ paths: mongo_initdb_root_password: type: string description: 'Mongo initdb root password' - 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' @@ -2684,6 +2687,9 @@ paths: mysql_root_password: type: string description: 'MySQL root password' + mysql_password: + type: string + description: 'MySQL password' mysql_user: type: string description: 'MySQL user' @@ -3311,7 +3317,7 @@ paths: type: string responses: '200': - description: 'Project details' + description: 'Environment details' content: application/json: schema: @@ -3467,9 +3473,7 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/PrivateKey' + $ref: '#/components/schemas/PrivateKey' '401': $ref: '#/components/responses/401' '400': @@ -3579,6 +3583,11 @@ paths: type: boolean example: false description: 'Instant validate.' + proxy_type: + type: string + enum: [traefik, caddy, none] + example: traefik + description: 'The proxy type.' type: object responses: '201': @@ -3668,6 +3677,14 @@ paths: summary: Update description: 'Update Server.' operationId: update-server-by-uuid + parameters: + - + name: uuid + in: path + description: 'Server UUID' + required: true + schema: + type: string requestBody: description: 'Server updated.' required: true @@ -3699,6 +3716,10 @@ paths: instant_validate: type: boolean description: 'Instant validate.' + proxy_type: + type: string + enum: [traefik, caddy, none] + description: 'The proxy type.' type: object responses: '201': @@ -3706,9 +3727,7 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/Server' + $ref: '#/components/schemas/Server' '401': $ref: '#/components/responses/401' '400': @@ -4759,6 +4778,10 @@ components: compose_parsing_version: type: string description: 'How Coolify parse the compose file.' + custom_nginx_configuration: + type: string + nullable: true + description: 'Custom Nginx configuration base64 encoded.' type: object ApplicationDeploymentQueue: description: 'Project model' @@ -4909,36 +4932,55 @@ components: properties: id: type: integer + description: 'The server ID.' uuid: type: string + description: 'The server UUID.' name: type: string + description: 'The server name.' description: type: string + description: 'The server description.' ip: type: string + description: 'The IP address.' user: type: string + description: 'The user.' port: type: integer + description: 'The port number.' proxy: type: object + description: 'The proxy configuration.' + proxy_type: + type: string + enum: + - traefik + - caddy + - none + description: 'The proxy type.' high_disk_usage_notification_sent: type: boolean + description: 'The flag to indicate if the high disk usage notification has been sent.' unreachable_notification_sent: type: boolean + description: 'The flag to indicate if the unreachable notification has been sent.' unreachable_count: type: integer + description: 'The unreachable count for your server.' validation_logs: type: string + description: 'The validation logs.' log_drain_notification_sent: type: boolean + description: 'The flag to indicate if the log drain notification has been sent.' swarm_cluster: type: string - delete_unused_volumes: - type: boolean - delete_unused_networks: - type: boolean + description: 'The swarm cluster configuration.' + settings: + $ref: '#/components/schemas/ServerSetting' type: object ServerSetting: description: 'Server Settings model' @@ -5011,6 +5053,12 @@ components: type: string updated_at: type: string + delete_unused_volumes: + type: boolean + description: 'The flag to indicate if the unused volumes should be deleted.' + delete_unused_networks: + type: boolean + description: 'The flag to indicate if the unused networks should be deleted.' type: object Service: description: 'Service model' @@ -5136,6 +5184,9 @@ components: smtp_notifications_database_backups: type: boolean description: 'Whether to send database backup notifications via SMTP.' + smtp_notifications_server_disk_usage: + type: boolean + description: 'Whether to send server disk usage notifications via SMTP.' discord_enabled: type: boolean description: 'Whether Discord is enabled or not.' @@ -5157,6 +5208,9 @@ components: discord_notifications_scheduled_tasks: type: boolean description: 'Whether to send scheduled task notifications via Discord.' + discord_notifications_server_disk_usage: + type: boolean + description: 'Whether to send server disk usage notifications via Discord.' show_boarding: type: boolean description: 'Whether to show the boarding screen or not.' diff --git a/other/nightly/docker-compose.prod.yml b/other/nightly/docker-compose.prod.yml index b15a109c3..d86b2336b 100644 --- a/other/nightly/docker-compose.prod.yml +++ b/other/nightly/docker-compose.prod.yml @@ -29,6 +29,7 @@ services: - REDIS_HOST - REDIS_PASSWORD - HORIZON_BALANCE + - HORIZON_MIN_PROCESSES - HORIZON_MAX_PROCESSES - HORIZON_BALANCE_MAX_SHIFT - HORIZON_BALANCE_COOLDOWN @@ -50,29 +51,8 @@ services: - TERMINAL_HOST - TERMINAL_PORT - AUTOUPDATE - - SELF_HOSTED - SSH_MUX_ENABLED - SSH_MUX_PERSIST_TIME - - FEEDBACK_DISCORD_WEBHOOK - - WAITLIST - - SUBSCRIPTION_PROVIDER - - STRIPE_API_KEY - - STRIPE_WEBHOOK_SECRET - - STRIPE_PRICE_ID_BASIC_MONTHLY - - STRIPE_PRICE_ID_BASIC_YEARLY - - STRIPE_PRICE_ID_PRO_MONTHLY - - STRIPE_PRICE_ID_PRO_YEARLY - - STRIPE_PRICE_ID_ULTIMATE_MONTHLY - - STRIPE_PRICE_ID_ULTIMATE_YEARLY - - STRIPE_PRICE_ID_DYNAMIC_MONTHLY - - STRIPE_PRICE_ID_DYNAMIC_YEARLY - - STRIPE_PRICE_ID_BASIC_MONTHLY_OLD - - STRIPE_PRICE_ID_BASIC_YEARLY_OLD - - STRIPE_PRICE_ID_PRO_MONTHLY_OLD - - STRIPE_PRICE_ID_PRO_YEARLY_OLD - - STRIPE_PRICE_ID_ULTIMATE_MONTHLY_OLD - - STRIPE_PRICE_ID_ULTIMATE_YEARLY_OLD - - STRIPE_EXCLUDED_PLANS ports: - "${APP_PORT:-8000}:80" expose: @@ -113,7 +93,7 @@ services: retries: 10 timeout: 2s soketi: - image: 'ghcr.io/coollabsio/coolify-realtime:1.0.3' + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.4' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002" diff --git a/other/nightly/install.sh b/other/nightly/install.sh index 04faf50ea..4a03a5c98 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -9,11 +9,11 @@ CDN="https://cdn.coollabs.io/coolify-nightly" DATE=$(date +"%Y%m%d-%H%M%S") VERSION="1.6" -DOCKER_VERSION="26.0" +DOCKER_VERSION="27.0" # TODO: Ask for a user CURRENT_USER=$USER -mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs} +mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel} mkdir -p /data/coolify/ssh/{keys,mux} mkdir -p /data/coolify/proxy/dynamic @@ -164,7 +164,6 @@ sles | opensuse-leap | opensuse-tumbleweed) esac - echo -e "2. Check OpenSSH server configuration. " # Detect OpenSSH server @@ -186,11 +185,51 @@ elif [ -x "$(command -v service)" ]; then SSH_DETECTED=true fi fi + + if [ "$SSH_DETECTED" = "false" ]; then - echo "###############################################################################" - echo "WARNING: Could not detect if OpenSSH server is installed and running - this does not mean that it is not installed, just that we could not detect it." - echo -e "Please make sure it is set, otherwise Coolify cannot connect to the host system. \n" - echo "###############################################################################" + echo " - OpenSSH server not detected. Installing OpenSSH server." + case "$OS_TYPE" in + arch) + pacman -Sy --noconfirm openssh >/dev/null + systemctl enable sshd >/dev/null 2>&1 + systemctl start sshd >/dev/null 2>&1 + ;; + alpine) + apk add openssh >/dev/null + rc-update add sshd default >/dev/null 2>&1 + service sshd start >/dev/null 2>&1 + ;; + ubuntu | debian | raspbian) + apt-get update -y >/dev/null + apt-get install -y openssh-server >/dev/null + systemctl enable ssh >/dev/null 2>&1 + systemctl start ssh >/dev/null 2>&1 + ;; + centos | fedora | rhel | ol | rocky | almalinux | amzn) + if [ "$OS_TYPE" = "amzn" ]; then + dnf install -y openssh-server >/dev/null + else + dnf install -y openssh-server >/dev/null + fi + systemctl enable sshd >/dev/null 2>&1 + systemctl start sshd >/dev/null 2>&1 + ;; + sles | opensuse-leap | opensuse-tumbleweed) + zypper install -y openssh >/dev/null + systemctl enable sshd >/dev/null 2>&1 + systemctl start sshd >/dev/null 2>&1 + ;; + *) + echo "###############################################################################" + echo "WARNING: Could not detect and install OpenSSH server - this does not mean that it is not installed or not running, just that we could not detect it." + echo -e "Please make sure it is installed and running, otherwise Coolify cannot connect to the host system. \n" + echo "###############################################################################" + exit 1 + ;; + esac + echo " - OpenSSH server installed successfully." + SSH_DETECTED=true fi # Detect SSH PermitRootLogin @@ -262,9 +301,14 @@ if ! [ -x "$(command -v docker)" ]; then fi ;; *) - curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh >/dev/null 2>&1 + if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then + echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)." + echo "Please install Docker manually." + exit 1 + fi + curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1 if ! [ -x "$(command -v docker)" ]; then - curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} >/dev/null 2>&1 + curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1 if ! [ -x "$(command -v docker)" ]; then echo " - Docker installation failed." echo " Maybe your OS is not supported?" @@ -287,7 +331,10 @@ test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon "log-opts": { "max-size": "10m", "max-file": "3" - } + }, + "default-address-pools": [ + {"base":"10.0.0.0/8","size":24} + ] } EOL cat >/etc/docker/daemon.json.coolify </etc/docker/daemon.json.coolify </dev/null 2>&1 +bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" echo " - Coolify installed successfully." rm -f $ENV_FILE-$DATE diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh index 9aa3a5f9a..670072b12 100644 --- a/other/nightly/upgrade.sh +++ b/other/nightly/upgrade.sh @@ -1,11 +1,14 @@ #!/bin/bash ## Do not modify this file. You will lose the ability to autoupdate! -VERSION="1.1" +VERSION="13" CDN="https://cdn.coollabs.io/coolify-nightly" LATEST_IMAGE=${1:-latest} LATEST_HELPER_VERSION=${2:-latest} +DATE=$(date +%Y-%m-%d-%H-%M-%S) +LOGFILE="/data/coolify/source/upgrade-${DATE}.log" + curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production @@ -31,8 +34,8 @@ docker network create --attachable coolify 2>/dev/null # docker network create --attachable --driver=overlay coolify-overlay 2>/dev/null if [ -f /data/coolify/source/docker-compose.custom.yml ]; then - echo "docker-compose.custom.yml detected." - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION:-latest} bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" + echo "docker-compose.custom.yml detected." >> $LOGFILE + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >> $LOGFILE 2>&1 else - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION:-latest} bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >> $LOGFILE 2>&1 fi diff --git a/other/nightly/versions.json b/other/nightly/versions.json index c04a3dee6..8b10875d0 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,16 +1,19 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.354" + "version": "4.0.0-beta.367" }, "nightly": { - "version": "4.0.0-beta.355" + "version": "4.0.0-beta.368" }, "helper": { - "version": "1.0.2" + "version": "1.0.3" }, "realtime": { - "version": "1.0.3" + "version": "1.0.4" + }, + "sentinel": { + "version": "0.0.15" } } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index adb1dc65a..22398dcf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,33 +1,30 @@ { - "name": "html", + "name": "coolify", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "coolify", "dependencies": { - "@tailwindcss/forms": "0.5.7", - "@tailwindcss/typography": "0.5.13", + "@tailwindcss/forms": "0.5.9", + "@tailwindcss/typography": "0.5.15", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", - "alpinejs": "3.14.0", - "cookie": "^0.7.0", - "dotenv": "^16.4.5", - "ioredis": "5.4.1", - "node-pty": "^1.0.0", - "tailwindcss-scrollbar": "0.1.0", - "ws": "^8.17.0" + "alpinejs": "3.14.3", + "ioredis": "5.4.1" }, "devDependencies": { - "@vitejs/plugin-vue": "4.5.1", - "autoprefixer": "10.4.19", - "axios": "1.7.5", - "laravel-echo": "1.16.1", - "laravel-vite-plugin": "0.8.1", - "postcss": "8.4.38", + "@vitejs/plugin-vue": "5.2.0", + "autoprefixer": "10.4.20", + "axios": "1.7.7", + "laravel-echo": "1.17.0", + "laravel-vite-plugin": "1.0.6", + "postcss": "8.4.49", "pusher-js": "8.4.0-rc2", - "tailwindcss": "3.4.4", - "vite": "4.5.5", - "vue": "3.4.29" + "tailwind-scrollbar": "^3.1.0", + "tailwindcss": "3.4.14", + "vite": "5.4.11", + "vue": "3.5.12" } }, "node_modules/@alloc/quick-lru": { @@ -41,11 +38,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -53,14 +74,46 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -70,13 +123,14 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -86,13 +140,14 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -102,13 +157,14 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -118,13 +174,14 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -134,13 +191,14 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -150,13 +208,14 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -166,13 +225,14 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -182,13 +242,14 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -198,13 +259,14 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -214,13 +276,14 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -230,13 +293,14 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -246,13 +310,14 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -262,13 +327,14 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -278,13 +344,14 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -294,13 +361,14 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -310,13 +378,14 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -326,13 +395,14 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -342,13 +412,14 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -358,13 +429,14 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -374,13 +446,14 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -390,13 +463,14 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -440,9 +514,10 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.18", @@ -490,21 +565,275 @@ "node": ">= 8" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.25.0.tgz", + "integrity": "sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.25.0.tgz", + "integrity": "sha512-/Y76tmLGUJqVBXXCfVS8Q8FJqYGhgH4wl4qTA24E9v/IJM0XvJCGQVSW1QZ4J+VURO9h8YCa28sTFacZXwK7Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.25.0.tgz", + "integrity": "sha512-YVT6L3UrKTlC0FpCZd0MGA7NVdp7YNaEqkENbWQ7AOVOqd/7VzyHpgIpc1mIaxRAo1ZsJRH45fq8j4N63I/vvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.25.0.tgz", + "integrity": "sha512-ZRL+gexs3+ZmmWmGKEU43Bdn67kWnMeWXLFhcVv5Un8FQcx38yulHBA7XR2+KQdYIOtD0yZDWBCudmfj6lQJoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.25.0.tgz", + "integrity": "sha512-xpEIXhiP27EAylEpreCozozsxWQ2TJbOLSivGfXhU4G1TBVEYtUPi2pOZBnvGXHyOdLAUUhPnJzH3ah5cqF01g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.25.0.tgz", + "integrity": "sha512-sC5FsmZGlJv5dOcURrsnIK7ngc3Kirnx3as2XU9uER+zjfyqIjdcMVgzy4cOawhsssqzoAX19qmxgJ8a14Qrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.25.0.tgz", + "integrity": "sha512-uD/dbLSs1BEPzg564TpRAQ/YvTnCds2XxyOndAO8nJhaQcqQGFgv/DAVko/ZHap3boCvxnzYMa3mTkV/B/3SWA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.25.0.tgz", + "integrity": "sha512-ZVt/XkrDlQWegDWrwyC3l0OfAF7yeJUF4fq5RMS07YM72BlSfn2fQQ6lPyBNjt+YbczMguPiJoCfaQC2dnflpQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.25.0.tgz", + "integrity": "sha512-qboZ+T0gHAW2kkSDPHxu7quaFaaBlynODXpBVnPxUgvWYaE84xgCKAPEYE+fSMd3Zv5PyFZR+L0tCdYCMAtG0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.25.0.tgz", + "integrity": "sha512-ndWTSEmAaKr88dBuogGH2NZaxe7u2rDoArsejNslugHZ+r44NfWiwjzizVS1nUOHo+n1Z6qV3X60rqE/HlISgw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.25.0.tgz", + "integrity": "sha512-BVSQvVa2v5hKwJSy6X7W1fjDex6yZnNKy3Kx1JGimccHft6HV0THTwNtC2zawtNXKUu+S5CjXslilYdKBAadzA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.25.0.tgz", + "integrity": "sha512-G4hTREQrIdeV0PE2JruzI+vXdRnaK1pg64hemHq2v5fhv8C7WjVaeXc9P5i4Q5UC06d/L+zA0mszYIKl+wY8oA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.25.0.tgz", + "integrity": "sha512-9T/w0kQ+upxdkFL9zPVB6zy9vWW1deA3g8IauJxojN4bnz5FwSsUAD034KpXIVX5j5p/rn6XqumBMxfRkcHapQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.25.0.tgz", + "integrity": "sha512-ThcnU0EcMDn+J4B9LD++OgBYxZusuA7iemIIiz5yzEcFg04VZFzdFjuwPdlURmYPZw+fgVrFzj4CA64jSTG4Ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.25.0.tgz", + "integrity": "sha512-zx71aY2oQxGxAT1JShfhNG79PnjYhMC6voAjzpu/xmMjDnKNf6Nl/xv7YaB/9SIa9jDYf8RBPWEnjcdlhlv1rQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.25.0.tgz", + "integrity": "sha512-JT8tcjNocMs4CylWY/CxVLnv8e1lE7ff1fi6kbGocWwxDq9pj30IJ28Peb+Y8yiPNSF28oad42ApJB8oUkwGww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.25.0.tgz", + "integrity": "sha512-dRLjLsO3dNOfSN6tjyVlG+Msm4IiZnGkuZ7G5NmpzwF9oOc582FZG05+UdfTbz5Jd4buK/wMb6UeHFhG18+OEg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.25.0.tgz", + "integrity": "sha512-/RqrIFtLB926frMhZD0a5oDa4eFIbyNEwLLloMTEjmqfwZWXywwVVOVmwTsuyhC9HKkVEZcOOi+KV4U9wmOdlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@tailwindcss/forms": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz", - "integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz", + "integrity": "sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==", + "license": "MIT", "dependencies": { "mini-svg-data-uri": "^1.2.3" }, "peerDependencies": { - "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20" } }, "node_modules/@tailwindcss/typography": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz", - "integrity": "sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==", + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz", + "integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==", + "license": "MIT", "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", @@ -512,7 +841,7 @@ "postcss-selector-parser": "6.0.10" }, "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20" } }, "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { @@ -527,92 +856,108 @@ "node": ">=4" } }, - "node_modules/@vitejs/plugin-vue": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.1.tgz", - "integrity": "sha512-DaUzYFr+2UGDG7VSSdShKa9sIWYBa1LL8KC0MNOf2H5LjcTPjob0x8LbkqXWmAtbANJCkpiQTj66UVcQkN2s3g==", + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.0.tgz", + "integrity": "sha512-7n7KdUEtx/7Yl7I/WVAMZ1bEb0eVvXF3ummWTeLcs/9gvo9pJhuLdouSXGjdZ/MKD1acf1I272+X0RMua4/R3g==", + "dev": true, + "license": "MIT", "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.0.0 || >=20.0.0" }, "peerDependencies": { - "vite": "^4.0.0 || ^5.0.0", + "vite": "^5.0.0", "vue": "^3.2.25" } }, "node_modules/@vue/compiler-core": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.29.tgz", - "integrity": "sha512-TFKiRkKKsRCKvg/jTSSKK7mYLJEQdUiUfykbG49rubC9SfDyvT2JrzTReopWlz2MxqeLyxh9UZhvxEIBgAhtrg==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", + "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/parser": "^7.24.7", - "@vue/shared": "3.4.29", + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.12", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-core/node_modules/@vue/shared": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", - "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", - "dev": true + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", + "dev": true, + "license": "MIT" }, "node_modules/@vue/compiler-dom": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.29.tgz", - "integrity": "sha512-A6+iZ2fKIEGnfPJejdB7b1FlJzgiD+Y/sxxKwJWg1EbJu6ZPgzaPQQ51ESGNv0CP6jm6Z7/pO6Ia8Ze6IKrX7w==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", + "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.4.29", - "@vue/shared": "3.4.29" + "@vue/compiler-core": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/compiler-dom/node_modules/@vue/shared": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", - "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", - "dev": true + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", + "dev": true, + "license": "MIT" }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.29.tgz", - "integrity": "sha512-zygDcEtn8ZimDlrEQyLUovoWgKQic6aEQqRXce2WXBvSeHbEbcAsXyCk9oG33ZkyWH4sl9D3tkYc1idoOkdqZQ==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", + "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/parser": "^7.24.7", - "@vue/compiler-core": "3.4.29", - "@vue/compiler-dom": "3.4.29", - "@vue/compiler-ssr": "3.4.29", - "@vue/shared": "3.4.29", + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.12", + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12", "estree-walker": "^2.0.2", - "magic-string": "^0.30.10", - "postcss": "^8.4.38", + "magic-string": "^0.30.11", + "postcss": "^8.4.47", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-sfc/node_modules/@vue/shared": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", - "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", - "dev": true + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", + "dev": true, + "license": "MIT" }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.29.tgz", - "integrity": "sha512-rFbwCmxJ16tDp3N8XCx5xSQzjhidYjXllvEcqX/lopkoznlNPz3jyy0WGJCyhAaVQK677WWFt3YO/WUEkMMUFQ==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", + "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.4.29", - "@vue/shared": "3.4.29" + "@vue/compiler-dom": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/compiler-ssr/node_modules/@vue/shared": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", - "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", - "dev": true + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", + "dev": true, + "license": "MIT" }, "node_modules/@vue/reactivity": { "version": "3.1.5", @@ -623,75 +968,83 @@ } }, "node_modules/@vue/runtime-core": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.29.tgz", - "integrity": "sha512-s8fmX3YVR/Rk5ig0ic0NuzTNjK2M7iLuVSZyMmCzN/+Mjuqqif1JasCtEtmtoJWF32pAtUjyuT2ljNKNLeOmnQ==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz", + "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/reactivity": "3.4.29", - "@vue/shared": "3.4.29" + "@vue/reactivity": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/runtime-core/node_modules/@vue/reactivity": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.29.tgz", - "integrity": "sha512-w8+KV+mb1a8ornnGQitnMdLfE0kXmteaxLdccm2XwdFxXst4q/Z7SEboCV5SqJNpZbKFeaRBBJBhW24aJyGINg==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz", + "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/shared": "3.4.29" + "@vue/shared": "3.5.12" } }, "node_modules/@vue/runtime-core/node_modules/@vue/shared": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", - "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", - "dev": true + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", + "dev": true, + "license": "MIT" }, "node_modules/@vue/runtime-dom": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.29.tgz", - "integrity": "sha512-gI10atCrtOLf/2MPPMM+dpz3NGulo9ZZR9d1dWo4fYvm+xkfvRrw1ZmJ7mkWtiJVXSsdmPbcK1p5dZzOCKDN0g==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz", + "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/reactivity": "3.4.29", - "@vue/runtime-core": "3.4.29", - "@vue/shared": "3.4.29", + "@vue/reactivity": "3.5.12", + "@vue/runtime-core": "3.5.12", + "@vue/shared": "3.5.12", "csstype": "^3.1.3" } }, "node_modules/@vue/runtime-dom/node_modules/@vue/reactivity": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.29.tgz", - "integrity": "sha512-w8+KV+mb1a8ornnGQitnMdLfE0kXmteaxLdccm2XwdFxXst4q/Z7SEboCV5SqJNpZbKFeaRBBJBhW24aJyGINg==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz", + "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/shared": "3.4.29" + "@vue/shared": "3.5.12" } }, "node_modules/@vue/runtime-dom/node_modules/@vue/shared": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", - "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", - "dev": true + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", + "dev": true, + "license": "MIT" }, "node_modules/@vue/server-renderer": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.29.tgz", - "integrity": "sha512-HMLCmPI2j/k8PVkSBysrA2RxcxC5DgBiCdj7n7H2QtR8bQQPqKAe8qoaxLcInzouBmzwJ+J0x20ygN/B5mYBng==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz", + "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.4.29", - "@vue/shared": "3.4.29" + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12" }, "peerDependencies": { - "vue": "3.4.29" + "vue": "3.5.12" } }, "node_modules/@vue/server-renderer/node_modules/@vue/shared": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", - "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", - "dev": true + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", + "dev": true, + "license": "MIT" }, "node_modules/@vue/shared": { "version": "3.1.5", @@ -712,9 +1065,10 @@ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" }, "node_modules/alpinejs": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.0.tgz", - "integrity": "sha512-YCWF95PMJqePe9ll6KMyDt/nLhh2R7RhqBf4loEmLzIskcHque4Br/9UgAa6cw13H0Cm3FM9e1hzDwP5z5wlDA==", + "version": "3.14.3", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.3.tgz", + "integrity": "sha512-cL8JBEDAm4UeVjTN5QnFl8QgMGUwxFn1GvQvu3RtfAHUrAPRahGihrsWpKnEK9L0QMqsAPk/R8MylMWKHaK33A==", + "license": "MIT", "dependencies": { "@vue/reactivity": "~3.1.1" } @@ -748,9 +1102,9 @@ "dev": true }, "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -766,12 +1120,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -785,9 +1140,9 @@ } }, "node_modules/axios": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", - "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -830,9 +1185,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "dev": true, "funding": [ { @@ -848,11 +1203,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -870,9 +1226,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001600", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz", - "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==", + "version": "1.0.30001680", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", + "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", "dev": true, "funding": [ { @@ -887,7 +1243,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chokidar": { "version": "3.5.3", @@ -959,14 +1316,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, - "node_modules/cookie": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.0.tgz", - "integrity": "sha512-qCf+V4dtlNhSRXGAZatc1TasyFO6GjohcOul807YOb5ik3+kQSnb4d7iajeCL8QHaJ4uZEjCgiCJerKXwdRVlQ==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -982,7 +1331,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/debug": { "version": "4.3.4", @@ -1027,29 +1377,19 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, - "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/electron-to-chromium": { - "version": "1.4.692", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.692.tgz", - "integrity": "sha512-d5rZRka9n2Y3MkWRN74IoAsxR0HK3yaAt7T50e3iT9VZmCCQDT3geXUO5ZRMhDToa1pkCeQXuNo+0g+NfDOVPA==", - "dev": true + "version": "1.5.55", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.55.tgz", + "integrity": "sha512-6maZ2ASDOTBtjt9FhqYPRnbvKU5tjG0IN9SztUOWYw2AzNDNpKJYLJmlK0/En4Hs/aiWnB+JZ+gW19PIGszgKg==", + "dev": true, + "license": "ISC" }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -1058,11 +1398,12 @@ } }, "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -1070,35 +1411,37 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1107,7 +1450,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -1368,28 +1712,33 @@ } }, "node_modules/laravel-echo": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.16.1.tgz", - "integrity": "sha512-++Ylb6M3ariC9Rk5WE5gZjj6wcEV5kvLF8b+geJ5/rRIfdoOA+eG6b9qJPrarMD9rY28Apx+l3eelIrCc2skVg==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.17.0.tgz", + "integrity": "sha512-uf+BVZMkXc7+pzxS2dG5v1P+MT3yWS+/9oDSJUcQ4KqnDKLYfM1lc7yUmnxvLtwPksGuQJv6XBtzvWLHSEheNQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/laravel-vite-plugin": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.8.1.tgz", - "integrity": "sha512-fxzUDjOA37kOsYq8dP+3oPIlw8/kJVXwu0hOXLun82R1LpV02shGeWGYKx2lbpKffL5I0sfPPjfqbYxuqBluAA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.0.6.tgz", + "integrity": "sha512-B34OqmZc/rV1KvSjst8SsUm/LKHsuDusw8jiZCIhlnTHXbXnK89JUM9pTJuk6E/Vc/1DT2gX7qNfhipak1WS8w==", "dev": true, + "license": "MIT", "dependencies": { "picocolors": "^1.0.0", - "vite-plugin-full-reload": "^1.0.5" + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" }, "engines": { - "node": ">=14" + "node": "^18.0.0 || >=20.0.0" }, "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0" + "vite": "^5.0.0" } }, "node_modules/lilconfig": { @@ -1431,12 +1780,13 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/merge2": { @@ -1514,11 +1864,6 @@ "thenify-all": "^1.0.0" } }, - "node_modules/nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" - }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -1536,20 +1881,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/node-pty": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", - "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", - "hasInstallScript": true, - "dependencies": { - "nan": "^2.17.0" - } - }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -1606,9 +1943,10 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -1638,9 +1976,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -1655,10 +1993,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -1859,18 +2198,40 @@ } }, "node_modules/rollup": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", - "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.25.0.tgz", + "integrity": "sha512-uVbClXmR6wvx5R1M3Od4utyLUxrmOcEm3pAtMphn73Apq19PDtHpgZoEvqH2YnnaNUuvKmg2DgRd2Sqv+odyqg==", "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.25.0", + "@rollup/rollup-android-arm64": "4.25.0", + "@rollup/rollup-darwin-arm64": "4.25.0", + "@rollup/rollup-darwin-x64": "4.25.0", + "@rollup/rollup-freebsd-arm64": "4.25.0", + "@rollup/rollup-freebsd-x64": "4.25.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.25.0", + "@rollup/rollup-linux-arm-musleabihf": "4.25.0", + "@rollup/rollup-linux-arm64-gnu": "4.25.0", + "@rollup/rollup-linux-arm64-musl": "4.25.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.25.0", + "@rollup/rollup-linux-riscv64-gnu": "4.25.0", + "@rollup/rollup-linux-s390x-gnu": "4.25.0", + "@rollup/rollup-linux-x64-gnu": "4.25.0", + "@rollup/rollup-linux-x64-musl": "4.25.0", + "@rollup/rollup-win32-arm64-msvc": "4.25.0", + "@rollup/rollup-win32-ia32-msvc": "4.25.0", + "@rollup/rollup-win32-x64-msvc": "4.25.0", "fsevents": "~2.3.2" } }, @@ -1916,9 +2277,10 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -1960,10 +2322,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-scrollbar": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.1.0.tgz", + "integrity": "sha512-pmrtDIZeHyu2idTejfV59SbaJyvp1VRjYxAjZBH0jnyrPRo6HL1kD5Glz8VPagasqr6oAx6M05+Tuw429Z8jxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "tailwindcss": "3.x" + } + }, "node_modules/tailwindcss": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", - "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz", + "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==", + "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -1996,14 +2372,6 @@ "node": ">=14.0.0" } }, - "node_modules/tailwindcss-scrollbar": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/tailwindcss-scrollbar/-/tailwindcss-scrollbar-0.1.0.tgz", - "integrity": "sha512-egipxw4ooQDh94x02XQpPck0P0sfwazwoUGfA9SedPATIuYDR+6qe8d31Gl7YsSMRiOKDkkqfI0kBvEw9lT/Hg==", - "peerDependencies": { - "tailwindcss": ">= 2.x.x" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -2046,9 +2414,9 @@ "dev": true }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -2064,9 +2432,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -2081,32 +2450,34 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", - "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": ">= 14", + "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -2124,6 +2495,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -2146,16 +2520,17 @@ } }, "node_modules/vue": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.29.tgz", - "integrity": "sha512-8QUYfRcYzNlYuzKPfge1UWC6nF9ym0lx7mpGVPJYNhddxEf3DD0+kU07NTL0sXuiT2HuJuKr/iEO8WvXvT0RSQ==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz", + "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.4.29", - "@vue/compiler-sfc": "3.4.29", - "@vue/runtime-dom": "3.4.29", - "@vue/server-renderer": "3.4.29", - "@vue/shared": "3.4.29" + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-sfc": "3.5.12", + "@vue/runtime-dom": "3.5.12", + "@vue/server-renderer": "3.5.12", + "@vue/shared": "3.5.12" }, "peerDependencies": { "typescript": "*" @@ -2167,36 +2542,17 @@ } }, "node_modules/vue/node_modules/@vue/shared": { - "version": "3.4.29", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", - "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", - "dev": true + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", + "dev": true, + "license": "MIT" }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/yaml": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", diff --git a/package.json b/package.json index 29f8f1a37..a174d73c7 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "name": "coolify", "private": true, "type": "module", "scripts": { @@ -6,28 +7,24 @@ "build": "vite build" }, "devDependencies": { - "@vitejs/plugin-vue": "4.5.1", - "autoprefixer": "10.4.19", - "axios": "1.7.5", - "laravel-echo": "1.16.1", - "laravel-vite-plugin": "0.8.1", - "postcss": "8.4.38", + "@vitejs/plugin-vue": "5.2.0", + "autoprefixer": "10.4.20", + "axios": "1.7.7", + "laravel-echo": "1.17.0", + "laravel-vite-plugin": "1.0.6", + "postcss": "8.4.49", "pusher-js": "8.4.0-rc2", - "tailwindcss": "3.4.4", - "vite": "4.5.5", - "vue": "3.4.29" + "tailwind-scrollbar": "^3.1.0", + "tailwindcss": "3.4.14", + "vite": "5.4.11", + "vue": "3.5.12" }, "dependencies": { - "@tailwindcss/forms": "0.5.7", - "@tailwindcss/typography": "0.5.13", + "@tailwindcss/forms": "0.5.9", + "@tailwindcss/typography": "0.5.15", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", - "alpinejs": "3.14.0", - "cookie": "^0.7.0", - "dotenv": "^16.4.5", - "ioredis": "5.4.1", - "node-pty": "^1.0.0", - "tailwindcss-scrollbar": "0.1.0", - "ws": "^8.17.0" + "alpinejs": "3.14.3", + "ioredis": "5.4.1" } -} \ No newline at end of file +} diff --git a/phpunit.xml b/phpunit.xml index 45cb69439..f1c2be92d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -13,8 +13,8 @@ - - + + diff --git a/public/.htaccess b/public/.htaccess deleted file mode 100644 index 3aec5e27e..000000000 --- a/public/.htaccess +++ /dev/null @@ -1,21 +0,0 @@ - - - Options -MultiViews -Indexes - - - RewriteEngine On - - # Handle Authorization Header - RewriteCond %{HTTP:Authorization} . - RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] - - # Redirect Trailing Slashes If Not A Folder... - RewriteCond %{REQUEST_FILENAME} !-d - RewriteCond %{REQUEST_URI} (.+)/$ - RewriteRule ^ %1 [L,R=301] - - # Send Requests To Front Controller... - RewriteCond %{REQUEST_FILENAME} !-d - RewriteCond %{REQUEST_FILENAME} !-f - RewriteRule ^ index.php [L] - diff --git a/public/svgs/apprise.png b/public/svgs/apprise.png new file mode 100644 index 000000000..aa6824bed Binary files /dev/null and b/public/svgs/apprise.png differ diff --git a/public/svgs/beszel.svg b/public/svgs/beszel.svg new file mode 100644 index 000000000..c6836479c --- /dev/null +++ b/public/svgs/beszel.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/convertx.png b/public/svgs/convertx.png new file mode 100644 index 000000000..7f4c41e2e Binary files /dev/null and b/public/svgs/convertx.png differ diff --git a/public/svgs/cyberchef.jpeg b/public/svgs/cyberchef.jpeg new file mode 100644 index 000000000..0405ac395 Binary files /dev/null and b/public/svgs/cyberchef.jpeg differ diff --git a/public/svgs/dashy.png b/public/svgs/dashy.png new file mode 100644 index 000000000..78fabd257 Binary files /dev/null and b/public/svgs/dashy.png differ diff --git a/public/svgs/default.webp b/public/svgs/default.webp new file mode 100644 index 000000000..e47383469 Binary files /dev/null and b/public/svgs/default.webp differ diff --git a/public/svgs/faraday.png b/public/svgs/faraday.png new file mode 100644 index 000000000..0965efe8f Binary files /dev/null and b/public/svgs/faraday.png differ diff --git a/public/svgs/firefox.svg b/public/svgs/firefox.svg new file mode 100644 index 000000000..9a6371d47 --- /dev/null +++ b/public/svgs/firefox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/heimdall.svg b/public/svgs/heimdall.svg new file mode 100644 index 000000000..6ecfa8457 --- /dev/null +++ b/public/svgs/heimdall.svg @@ -0,0 +1,11 @@ + + + + background + + + + Layer 1 + + + \ No newline at end of file diff --git a/public/svgs/hoarder.svg b/public/svgs/hoarder.svg new file mode 100644 index 000000000..6215461d2 --- /dev/null +++ b/public/svgs/hoarder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/hoppscotch.png b/public/svgs/hoppscotch.png new file mode 100644 index 000000000..84c728437 Binary files /dev/null and b/public/svgs/hoppscotch.png differ diff --git a/public/svgs/invoiceninja.png b/public/svgs/invoiceninja.png new file mode 100644 index 000000000..5141cd4d1 Binary files /dev/null and b/public/svgs/invoiceninja.png differ diff --git a/public/svgs/jupyter.svg b/public/svgs/jupyter.svg new file mode 100644 index 000000000..ab2550874 --- /dev/null +++ b/public/svgs/jupyter.svg @@ -0,0 +1,90 @@ + +Group.svg +Created using Figma 0.90 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/kuzzle.png b/public/svgs/kuzzle.png new file mode 100644 index 000000000..a7bf37029 Binary files /dev/null and b/public/svgs/kuzzle.png differ diff --git a/public/svgs/macos.svg b/public/svgs/macos.svg new file mode 100644 index 000000000..483fa6a17 --- /dev/null +++ b/public/svgs/macos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/maybe.svg b/public/svgs/maybe.svg new file mode 100644 index 000000000..9a8aa75cb --- /dev/null +++ b/public/svgs/maybe.svg @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/mealie.png b/public/svgs/mealie.png new file mode 100644 index 000000000..74a2d7b62 Binary files /dev/null and b/public/svgs/mealie.png differ diff --git a/public/svgs/mosquitto.png b/public/svgs/mosquitto.png deleted file mode 100644 index eb287a7cd..000000000 Binary files a/public/svgs/mosquitto.png and /dev/null differ diff --git a/public/svgs/mosquitto.svg b/public/svgs/mosquitto.svg new file mode 100644 index 000000000..7e7dde5b6 --- /dev/null +++ b/public/svgs/mosquitto.svg @@ -0,0 +1 @@ + image/svg+xml \ No newline at end of file diff --git a/public/svgs/overseerr.svg b/public/svgs/overseerr.svg new file mode 100644 index 000000000..8116787c2 --- /dev/null +++ b/public/svgs/overseerr.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/svgs/pairdrop.png b/public/svgs/pairdrop.png new file mode 100644 index 000000000..b0e9ee5d0 Binary files /dev/null and b/public/svgs/pairdrop.png differ diff --git a/public/svgs/penpot.svg b/public/svgs/penpot.svg new file mode 100644 index 000000000..6439292bd --- /dev/null +++ b/public/svgs/penpot.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/svgs/plex.svg b/public/svgs/plex.svg new file mode 100644 index 000000000..872b135cf --- /dev/null +++ b/public/svgs/plex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/portainer.png b/public/svgs/portainer.png new file mode 100644 index 000000000..2a4010f17 Binary files /dev/null and b/public/svgs/portainer.png differ diff --git a/public/svgs/postiz.svg b/public/svgs/postiz.svg new file mode 100644 index 000000000..6e3baa813 --- /dev/null +++ b/public/svgs/postiz.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/svgs/privatebin.svg b/public/svgs/privatebin.svg new file mode 100644 index 000000000..d63c65dbd --- /dev/null +++ b/public/svgs/privatebin.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/prowlarr.svg b/public/svgs/prowlarr.svg new file mode 100644 index 000000000..a1a5a8ce3 --- /dev/null +++ b/public/svgs/prowlarr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/pterodactyl.png b/public/svgs/pterodactyl.png new file mode 100644 index 000000000..a5addb87c Binary files /dev/null and b/public/svgs/pterodactyl.png differ diff --git a/public/svgs/radarr.svg b/public/svgs/radarr.svg new file mode 100644 index 000000000..93a4c9232 --- /dev/null +++ b/public/svgs/radarr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/readeck.svg b/public/svgs/readeck.svg new file mode 100644 index 000000000..07f6e6157 --- /dev/null +++ b/public/svgs/readeck.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/svgs/redlib.svg b/public/svgs/redlib.svg new file mode 100644 index 000000000..16f73b5dd --- /dev/null +++ b/public/svgs/redlib.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/svgs/sonarr.svg b/public/svgs/sonarr.svg new file mode 100644 index 000000000..91c04e289 --- /dev/null +++ b/public/svgs/sonarr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/web-check.png b/public/svgs/web-check.png new file mode 100644 index 000000000..5b1384764 Binary files /dev/null and b/public/svgs/web-check.png differ diff --git a/public/svgs/whoogle.png b/public/svgs/whoogle.png new file mode 100644 index 000000000..0d89d25f2 Binary files /dev/null and b/public/svgs/whoogle.png differ diff --git a/public/svgs/wikijs.svg b/public/svgs/wikijs.svg new file mode 100644 index 000000000..52c4a790b --- /dev/null +++ b/public/svgs/wikijs.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/windows.svg b/public/svgs/windows.svg new file mode 100644 index 000000000..2c7392e9c --- /dev/null +++ b/public/svgs/windows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/css/app.css b/resources/css/app.css index 73f19de96..32d476c1a 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -4,11 +4,9 @@ @tailwind components; @tailwind utilities; - - html, body { - @apply h-full bg-neutral-50 text-neutral-800 dark:bg-base dark:text-neutral-400; + @apply h-full bg-neutral-50 text-neutral-800 dark:bg-base dark:text-neutral-400 w-full; } body { @@ -32,6 +30,14 @@ body { @apply text-black dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300; } +.input-sticky { + @apply text-black dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 dark:focus:ring-coolgray-300 focus:ring-neutral-400 block w-full py-1.5 rounded border-0 text-sm ring-1 ring-inset; +} + +.input-sticky-active { + @apply border-2 border-coollabs text-black dark:text-white focus:bg-neutral-200 dark:focus:bg-coolgray-400 focus:border-coollabs; +} + /* Readonly */ .input { @apply dark:read-only:text-neutral-500 dark:read-only:ring-0 dark:read-only:bg-coolgray-100/40 placeholder:text-neutral-300 dark:placeholder:text-neutral-700 read-only:text-neutral-500 read-only:bg-neutral-200; @@ -172,10 +178,6 @@ section { @apply bg-error; } -/* [type='checkbox']:checked { - background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='black' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); -} */ - .menu { @apply flex items-center gap-1; } @@ -197,7 +199,7 @@ section { } .scrollbar { - @apply scrollbar-thumb-coollabs-100 dark:scrollbar-track-coolgray-200 scrollbar-track-neutral-200 scrollbar-w-2; + @apply scrollbar-thumb-coollabs-100 dark:scrollbar-track-coolgray-200 scrollbar-track-neutral-200 scrollbar-thin; } .main { @@ -320,4 +322,4 @@ section { .dz-button { @apply w-full p-4 py-10 my-4 font-bold bg-white border dark:border-coolgray-400 dark:text-white dark:bg-transparent hover:dark:bg-coolgray-400; -} +} \ No newline at end of file diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js deleted file mode 100644 index 3d7608588..000000000 --- a/resources/js/bootstrap.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * We'll load the axios HTTP library which allows us to easily issue requests - * to our Laravel back-end. This library automatically handles sending the - * CSRF token as a header based on the value of the "XSRF" token cookie. - */ - -// import axios from 'axios'; -// window.axios = axios; - -// window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; - -/** - * Echo exposes an expressive API for subscribing to channels and listening - * for events that are broadcast by Laravel. Echo and event broadcasting - * allows your team to easily build robust real-time web applications. - */ - -// import Echo from 'laravel-echo'; - -// import Pusher from 'pusher-js'; -// window.Pusher = Pusher; - -// window.Echo = new Echo({ -// broadcaster: 'pusher', -// key: import.meta.env.VITE_PUSHER_APP_KEY, -// cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1', -// wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, -// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, -// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, -// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', -// enabledTransports: ['ws', 'wss'], -// }); diff --git a/resources/js/terminal.js b/resources/js/terminal.js index 59c9a79a8..e70a8cd1a 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -60,7 +60,22 @@ export function initializeTerminalComponent() { }; }, + resetTerminal() { + if (this.term) { + this.$wire.dispatch('error', 'Terminal websocket connection lost.'); + this.term.reset(); + this.term.clear(); + this.pendingWrites = 0; + this.paused = false; + this.commandBuffer = ''; + // Force a refresh + this.$nextTick(() => { + this.resizeTerminal(); + this.term.focus(); + }); + } + }, setupTerminal() { const terminalElement = document.getElementById('terminal'); if (terminalElement) { @@ -69,9 +84,15 @@ export function initializeTerminalComponent() { rows: 30, fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', cursorBlink: true, + rendererType: 'canvas', + convertEol: true, + disableStdin: false }); this.fitAddon = new FitAddon(); this.term.loadAddon(this.fitAddon); + this.$nextTick(() => { + this.resizeTerminal(); + }); } }, @@ -101,12 +122,19 @@ export function initializeTerminalComponent() { `${connectionString.protocol}://${connectionString.host}${connectionString.port}${connectionString.path}` this.socket = new WebSocket(url); + this.socket.onopen = () => { + console.log('[Terminal] WebSocket connection established. Cool cool cool cool cool cool.'); + }; + this.socket.onmessage = this.handleSocketMessage.bind(this); this.socket.onerror = (e) => { - console.error('WebSocket error:', e); + console.error('[Terminal] WebSocket error.'); }; this.socket.onclose = () => { - console.log('WebSocket connection closed'); + console.warn('[Terminal] WebSocket connection closed.'); + this.resetTerminal(); + this.message = '(connection closed)'; + this.terminalActive = false; this.reconnect(); }; } @@ -117,19 +145,18 @@ export function initializeTerminalComponent() { clearInterval(this.reconnectInterval); } this.reconnectInterval = setInterval(() => { - console.log('Attempting to reconnect...'); + console.warn('[Terminal] Attempting to reconnect...'); this.initializeWebSocket(); if (this.socket && this.socket.readyState === WebSocket.OPEN) { - console.log('Reconnected successfully'); + console.log('[Terminal] Reconnected successfully'); clearInterval(this.reconnectInterval); this.reconnectInterval = null; - window.location.reload(); + } }, 2000); }, handleSocketMessage(event) { - this.message = '(connection closed)'; if (event.data === 'pty-ready') { if (!this.term._initialized) { this.term.open(document.getElementById('terminal')); @@ -150,8 +177,17 @@ export function initializeTerminalComponent() { this.term.reset(); this.commandBuffer = ''; } else { - this.pendingWrites++; - this.term.write(event.data, this.flowControlCallback.bind(this)); + try { + this.pendingWrites++; + this.term.write(event.data, (err) => { + if (err) { + console.error('[Terminal] Write error:', err); + } + this.flowControlCallback(); + }); + } catch (error) { + console.error('[Terminal] Write operation failed:', error); + } } }, @@ -173,11 +209,15 @@ export function initializeTerminalComponent() { if (!this.term) return; this.term.onData((data) => { - this.socket.send(JSON.stringify({ message: data })); - if (data === '\r') { - this.commandBuffer = ''; + if (this.socket.readyState === WebSocket.OPEN) { + this.socket.send(JSON.stringify({ message: data })); + if (data === '\r') { + this.commandBuffer = ''; + } else { + this.commandBuffer += data; + } } else { - this.commandBuffer += data; + console.warn('[Terminal] WebSocket not ready, data not sent'); } }); diff --git a/resources/views/auth/forgot-password.blade.php b/resources/views/auth/forgot-password.blade.php index f66b460be..00cf95a44 100644 --- a/resources/views/auth/forgot-password.blade.php +++ b/resources/views/auth/forgot-password.blade.php @@ -18,7 +18,7 @@ @else
Transactional emails are not active on this instance.
See how to set it in our docs, or how to + href="{{ config('constants.urls.docs') }}">docs, or how to manually reset password.
@endif diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php index 316e73949..fb244962d 100644 --- a/resources/views/components/forms/checkbox.blade.php +++ b/resources/views/components/forms/checkbox.blade.php @@ -5,18 +5,20 @@ 'disabled' => false, 'instantSave' => false, 'value' => null, - 'domValue' => null, + 'checked' => false, 'hideLabel' => false, 'fullWidth' => false, ])
$fullWidth, ])> @if (!$hideLabel) - + @endif
diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index f0c42a71d..8495417ac 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -28,7 +28,7 @@ $disableTwoStepConfirmation = data_get(InstanceSettings::get(), 'disable_two_step_confirmation'); @endphp -
+}" @keydown.escape.window="modalOpen = false; resetModal()" + :class="{ 'z-40': modalOpen }" class="relative w-auto h-auto"> @if ($customButton) @if ($buttonFullWidth) @@ -302,7 +302,8 @@ @endif - - + + -

+

+

@error('password')

{{ $message }}

@enderror diff --git a/resources/views/components/modal-input.blade.php b/resources/views/components/modal-input.blade.php index f396f399d..acd3b8bdf 100644 --- a/resources/views/components/modal-input.blade.php +++ b/resources/views/components/modal-input.blade.php @@ -7,9 +7,10 @@ 'action' => 'delete', 'content' => null, 'closeOutside' => true, + 'minWidth' => '36rem', ])
+ class="relative w-auto h-auto" wire:ignore> @if ($content)
{{ $content }} @@ -27,7 +28,7 @@ @endif